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

LibreSign / libresign / 24083305764

07 Apr 2026 01:14PM UTC coverage: 55.599%. First build
24083305764

Pull #7450

github

web-flow
Merge 99a97f498 into 1112b1165
Pull Request #7450: chore(rector): apply safe test-only batch and php82 baseline

5 of 20 new or added lines in 15 files covered. (25.0%)

10233 of 18405 relevant lines covered (55.6%)

6.61 hits per line

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

53.41
/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');
69✔
27
        }
28

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

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

36
                /** @var Crl */
37
                return $this->findEntity($qb);
25✔
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();
59✔
70
                $certificate->setSerialNumber($serialNumber);
59✔
71
                $certificate->setOwner($owner);
59✔
72
                $certificate->setStatus(CRLStatus::ISSUED->value);
59✔
73
                $certificate->setIssuedAt($issuedAt);
59✔
74
                $certificate->setValidTo($validTo);
59✔
75
                $certificate->setEngine($engine);
59✔
76
                $certificate->setInstanceId($instanceId);
59✔
77
                $certificate->setGeneration($generation);
59✔
78
                $certificate->setIssuerFromArray($issuer);
59✔
79
                $certificate->setSubjectFromArray($subject);
59✔
80
                $certificate->setCertificateType($certificateType);
59✔
81

82
                /** @var Crl */
83
                return $this->insert($certificate);
59✔
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
                return $this->revokeCertificateEntity(
×
96
                        $certificate,
×
97
                        $reason,
×
98
                        $comment,
×
99
                        $revokedBy,
×
100
                        $invalidityDate,
×
101
                        $crlNumber
×
102
                );
×
103
        }
104

105
        public function revokeCertificateEntity(
106
                Crl $certificate,
107
                CRLReason $reason = CRLReason::UNSPECIFIED,
108
                ?string $comment = null,
109
                ?string $revokedBy = null,
110
                ?DateTime $invalidityDate = null,
111
                ?int $crlNumber = null,
112
        ): Crl {
113
                if (CRLStatus::from($certificate->getStatus()) !== CRLStatus::ISSUED) {
×
114
                        throw new \InvalidArgumentException('Certificate is not in issued status');
×
115
                }
116

117
                $certificate->setStatus(CRLStatus::REVOKED->value);
×
118
                $certificate->setReasonCode($reason->value);
×
119
                $certificate->setComment($comment !== '' ? $comment : null);
×
120
                $certificate->setRevokedBy($revokedBy);
×
121
                $certificate->setRevokedAt(new DateTime());
×
122
                $certificate->setInvalidityDate($invalidityDate);
×
123
                $certificate->setCrlNumber($crlNumber);
×
124

125
                /** @var Crl */
126
                return $this->update($certificate);
×
127
        }
128

129
        public function getRevokedCertificates(string $instanceId = '', int $generation = 0, string $engineType = ''): array {
130
                $qb = $this->db->getQueryBuilder();
2✔
131

132
                $qb->select('*')
2✔
133
                        ->from($this->getTableName())
2✔
134
                        ->where($qb->expr()->eq('status', $qb->createNamedParameter(CRLStatus::REVOKED->value)))
2✔
135
                        ->orderBy('revoked_at', 'DESC');
2✔
136

137
                if ($instanceId !== '') {
2✔
138
                        $qb->andWhere($qb->expr()->eq('instance_id', $qb->createNamedParameter($instanceId)));
2✔
139
                }
140
                if ($generation !== 0) {
2✔
141
                        $qb->andWhere($qb->expr()->eq('generation', $qb->createNamedParameter($generation, IQueryBuilder::PARAM_INT)));
2✔
142
                }
143
                if ($engineType !== '') {
2✔
144
                        $engineName = match($engineType) {
2✔
145
                                'o' => 'openssl',
2✔
146
                                'c' => 'cfssl',
×
147
                                'openssl', 'cfssl' => $engineType,
×
148
                                default => throw new \InvalidArgumentException("Invalid engine type: $engineType"),
×
149
                        };
2✔
150
                        $qb->andWhere($qb->expr()->eq('engine', $qb->createNamedParameter($engineName)));
2✔
151
                }
152

153
                return $this->findEntities($qb);
2✔
154
        }
155

156
        public function isInvalidAt(string $serialNumber, ?DateTime $checkDate = null): bool {
157
                $checkDate = $checkDate ?? new DateTime();
×
158

159
                try {
160
                        $certificate = $this->findBySerialNumber($serialNumber);
×
NEW
161
                } catch (DoesNotExistException $e) {
×
162
                        return false;
×
163
                }
164

165
                if ($certificate->isRevoked()) {
×
166
                        return true;
×
167
                }
168

169
                if ($certificate->getInvalidityDate() && $certificate->getInvalidityDate() <= $checkDate) {
×
170
                        return true;
×
171
                }
172

173
                return false;
×
174
        }
175

176
        public function cleanupExpiredCertificates(?DateTime $before = null): int {
177
                $before = $before ?? new DateTime('-1 year');
×
178

179
                $qb = $this->db->getQueryBuilder();
×
180

181
                return $qb->delete($this->getTableName())
×
182
                        ->where($qb->expr()->isNotNull('valid_to'))
×
183
                        ->andWhere($qb->expr()->lt('valid_to', $qb->createNamedParameter($before, 'datetime')))
×
184
                        ->executeStatement();
×
185
        }
186

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

190
                $result = $qb->select('status', $qb->func()->count('*', 'count'))
×
191
                        ->from($this->getTableName())
×
192
                        ->groupBy('status')
×
193
                        ->executeQuery();
×
194

195
                $stats = [];
×
196
                while ($row = $result->fetch()) {
×
197
                        $stats[$row['status']] = (int)$row['count'];
×
198
                }
199

200
                $result->closeCursor();
×
201
                return $stats;
×
202
        }
203

204
        public function getRevocationStatistics(): array {
205
                $qb = $this->db->getQueryBuilder();
×
206

207
                $result = $qb->select('reason_code', $qb->func()->count('*', 'count'))
×
208
                        ->from($this->getTableName())
×
209
                        ->where($qb->expr()->eq('status', $qb->createNamedParameter(CRLStatus::REVOKED->value)))
×
210
                        ->andWhere($qb->expr()->isNotNull('reason_code'))
×
211
                        ->groupBy('reason_code')
×
212
                        ->executeQuery();
×
213

214
                $stats = [];
×
215
                while ($row = $result->fetch()) {
×
216
                        $reasonCode = (int)$row['reason_code'];
×
217
                        $reason = CRLReason::tryFrom($reasonCode);
×
218
                        $stats[$reasonCode] = [
×
219
                                'code' => $reasonCode,
×
220
                                'description' => $reason?->getDescription() ?? 'unknown',
×
221
                                'count' => (int)$row['count'],
×
222
                        ];
×
223
                }
224

225
                $result->closeCursor();
×
226
                return $stats;
×
227
        }
228

229
        public function getLastCrlNumber(string $instanceId, int $generation, string $engineType): int {
230
                $qb = $this->db->getQueryBuilder();
2✔
231

232
                $qb->select($qb->func()->max('crl_number'))
2✔
233
                        ->from($this->getTableName())
2✔
234
                        ->where($qb->expr()->eq('instance_id', $qb->createNamedParameter($instanceId)))
2✔
235
                        ->andWhere($qb->expr()->eq('generation', $qb->createNamedParameter($generation, IQueryBuilder::PARAM_INT)))
2✔
236
                        ->andWhere($qb->expr()->eq('engine', $qb->createNamedParameter($engineType)))
2✔
237
                        ->andWhere($qb->expr()->isNotNull('crl_number'));
2✔
238

239
                $result = $qb->executeQuery();
2✔
240
                $maxCrlNumber = $result->fetchOne();
2✔
241
                $result->closeCursor();
2✔
242

243
                return (int)($maxCrlNumber ?? 0);
2✔
244
        }
245

246
        /**
247
         * List CRL entries with pagination and filters
248
         *
249
         * @param int $page Page number (1-based)
250
         * @param int $length Number of items per page
251
         * @param array<string, mixed> $filter Filters to apply (status, engine, instance_id, owner, etc.)
252
         * @param array<string, string> $sort Sort fields and directions ['field' => 'ASC|DESC']
253
         * @return array{data: array<Crl>, total: int}
254
         */
255
        public function listWithPagination(
256
                int $page = 1,
257
                int $length = 100,
258
                array $filter = [],
259
                array $sort = [],
260
        ): array {
261
                $qb = $this->db->getQueryBuilder();
5✔
262

263
                $qb->select('*')
5✔
264
                        ->from($this->getTableName());
5✔
265

266
                if (!empty($filter['status'])) {
5✔
267
                        $qb->andWhere($qb->expr()->eq('status', $qb->createNamedParameter($filter['status'])));
1✔
268
                }
269

270
                if (!empty($filter['engine'])) {
5✔
271
                        $qb->andWhere($qb->expr()->eq('engine', $qb->createNamedParameter($filter['engine'])));
1✔
272
                }
273

274
                if (!empty($filter['instance_id'])) {
5✔
275
                        $qb->andWhere($qb->expr()->eq('instance_id', $qb->createNamedParameter($filter['instance_id'])));
×
276
                }
277

278
                if (!empty($filter['generation'])) {
5✔
279
                        $qb->andWhere($qb->expr()->eq('generation', $qb->createNamedParameter((int)$filter['generation'], IQueryBuilder::PARAM_INT)));
×
280
                }
281

282
                if (!empty($filter['owner'])) {
5✔
283
                        $qb->andWhere($qb->expr()->like('owner', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['owner']) . '%')));
×
284
                }
285

286
                if (!empty($filter['serial_number'])) {
5✔
287
                        $qb->andWhere($qb->expr()->like('serial_number', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['serial_number']) . '%')));
×
288
                }
289

290
                if (!empty($filter['revoked_by'])) {
5✔
291
                        $qb->andWhere($qb->expr()->like('revoked_by', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['revoked_by']) . '%')));
×
292
                }
293

294
                $countQb = $this->db->getQueryBuilder();
5✔
295
                $countQb->select($countQb->func()->count('*', 'count'))
5✔
296
                        ->from($this->getTableName());
5✔
297

298
                if (!empty($filter['status'])) {
5✔
299
                        $countQb->andWhere($countQb->expr()->eq('status', $countQb->createNamedParameter($filter['status'])));
1✔
300
                }
301
                if (!empty($filter['engine'])) {
5✔
302
                        $countQb->andWhere($countQb->expr()->eq('engine', $countQb->createNamedParameter($filter['engine'])));
1✔
303
                }
304
                if (!empty($filter['instance_id'])) {
5✔
305
                        $countQb->andWhere($countQb->expr()->eq('instance_id', $countQb->createNamedParameter($filter['instance_id'])));
×
306
                }
307
                if (!empty($filter['generation'])) {
5✔
308
                        $countQb->andWhere($countQb->expr()->eq('generation', $countQb->createNamedParameter((int)$filter['generation'], IQueryBuilder::PARAM_INT)));
×
309
                }
310
                if (!empty($filter['owner'])) {
5✔
311
                        $countQb->andWhere($countQb->expr()->like('owner', $countQb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['owner']) . '%')));
×
312
                }
313
                if (!empty($filter['serial_number'])) {
5✔
314
                        $countQb->andWhere($countQb->expr()->like('serial_number', $countQb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['serial_number']) . '%')));
×
315
                }
316
                if (!empty($filter['revoked_by'])) {
5✔
317
                        $countQb->andWhere($countQb->expr()->like('revoked_by', $countQb->createNamedParameter('%' . $this->db->escapeLikeParameter($filter['revoked_by']) . '%')));
×
318
                }
319

320
                $total = (int)$countQb->executeQuery()->fetchOne();
5✔
321

322
                $allowedSortFields = [
5✔
323
                        'serial_number',
5✔
324
                        'owner',
5✔
325
                        'status',
5✔
326
                        'engine',
5✔
327
                        'issued_at',
5✔
328
                        'valid_to',
5✔
329
                        'revoked_at',
5✔
330
                        'reason_code',
5✔
331
                ];
5✔
332

333
                if (!empty($sort)) {
5✔
334
                        foreach ($sort as $field => $direction) {
1✔
335
                                if (!in_array($field, $allowedSortFields, true)) {
1✔
336
                                        continue;
×
337
                                }
338
                                $direction = strtoupper($direction) === 'DESC' ? 'DESC' : 'ASC';
1✔
339
                                $qb->addOrderBy($field, $direction);
1✔
340
                        }
341
                } else {
342
                        $qb->orderBy('revoked_at', 'DESC')
4✔
343
                                ->addOrderBy('issued_at', 'DESC');
4✔
344
                }
345

346
                $offset = ($page - 1) * $length;
5✔
347
                $qb->setFirstResult($offset)
5✔
348
                        ->setMaxResults($length);
5✔
349

350
                return [
5✔
351
                        'data' => $this->findEntities($qb),
5✔
352
                        'total' => $total,
5✔
353
                ];
5✔
354
        }
355
}
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