Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.12% covered (success)
94.12%
32 / 34
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
CrlDistributionPointsExtractor
94.12% covered (success)
94.12%
32 / 34
33.33% covered (danger)
33.33%
1 / 3
16.05
0.00% covered (danger)
0.00%
0 / 1
 extractFromExtensions
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
9
 appendUrlsFromText
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
6.03
 normalizeUrlToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5/**
6 * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
7 * SPDX-License-Identifier: AGPL-3.0-or-later
8 */
9
10namespace OCA\Libresign\Service\Crl;
11
12final class CrlDistributionPointsExtractor {
13    /** @var array<string, true> */
14    private const array ACCEPTED_EXTENSION_NAMES = [
15        'crldistributionpoints' => true,
16        'x509v3 crl distribution points' => true,
17        '2.5.29.31' => true,
18    ];
19
20    private const string URI_PATTERN = '/URI\s*:\s*([^\s\n]+)/i';
21
22    /**
23     * @param array<array-key, mixed> $extensions
24     * @return array{hasExtension: bool, urls: list<string>}
25     */
26    public function extractFromExtensions(array $extensions): array {
27        $hasCrlExtension = false;
28        $orderedUrls = [];
29        $seenUrls = [];
30        foreach ($extensions as $extensionName => $extensionValue) {
31            if (!is_string($extensionName)) {
32                continue;
33            }
34
35            $normalizedName = strtolower(trim($extensionName));
36            if (!isset(self::ACCEPTED_EXTENSION_NAMES[$normalizedName])) {
37                continue;
38            }
39            $hasCrlExtension = true;
40
41            if (is_string($extensionValue)) {
42                $this->appendUrlsFromText($extensionValue, $orderedUrls, $seenUrls);
43            } elseif (is_array($extensionValue)) {
44                foreach ($extensionValue as $extensionPart) {
45                    if (is_string($extensionPart)) {
46                        $this->appendUrlsFromText($extensionPart, $orderedUrls, $seenUrls);
47                    }
48                }
49            }
50        }
51
52        if (!$hasCrlExtension) {
53            return ['hasExtension' => false, 'urls' => []];
54        }
55
56        return [
57            'hasExtension' => true,
58            'urls' => $orderedUrls,
59        ];
60    }
61
62    /**
63     * @param list<string> $orderedUrls
64     * @param array<string, true> $seenUrls
65     */
66    private function appendUrlsFromText(string $value, array &$orderedUrls, array &$seenUrls): void {
67        if (stripos($value, 'URI') === false) {
68            return;
69        }
70
71        preg_match_all(self::URI_PATTERN, $value, $matches);
72        if (empty($matches[1])) {
73            return;
74        }
75
76        foreach ($matches[1] as $url) {
77            $normalizedUrl = $this->normalizeUrlToken($url);
78            if ($normalizedUrl === '' || isset($seenUrls[$normalizedUrl])) {
79                continue;
80            }
81
82            $seenUrls[$normalizedUrl] = true;
83            $orderedUrls[] = $normalizedUrl;
84        }
85    }
86
87    private function normalizeUrlToken(string $url): string {
88        return rtrim($url, ')]');
89    }
90}