Passed
Push — master ( e0f947...82ed42 )
by Michael
17:30 queued 07:57
created

PublicSuffixList::getMetadata()   C

Complexity

Conditions 12
Paths 49

Size

Total Lines 52
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 33
nc 49
nop 0
dl 0
loc 52
rs 6.9666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Xoops\RegDom;
6
7
/**
8
 * Manages and queries the Public Suffix List (PSL) from a pre-generated cache.
9
 *
10
 * @package   Xoops\RegDom
11
 * @author    Florian Sager, 06.08.2008, <[email protected]>
12
 * @author    Marcus Bointon (https://github.com/Synchro/regdom-php)
13
 * @author    Richard Griffith <[email protected]>
14
 * @author    Michael Beck <[email protected]>
15
 * @license   Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
16
 */
17
18
/**
19
 * @phpstan-type PslRules array{'NORMAL': array<string, true>, 'WILDCARD': array<string, true>, 'EXCEPTION': array<string, true>}
20
 */
21
22
class PublicSuffixList
23
{
24
    /**
25
     * @var array{'NORMAL': array<string, true>, 'WILDCARD': array<string, true>, 'EXCEPTION': array<string, true>}|null
26
     */
27
    private static ?array $rules = null;
28
29
    public function __construct()
30
    {
31
        if (self::$rules === null) {
32
            self::$rules = $this->loadRules();
33
        }
34
    }
35
36
    /**
37
     * @return array{'NORMAL': array<string, true>, 'WILDCARD': array<string, true>, 'EXCEPTION': array<string, true>}
38
     */
39
    private function loadRules(): array
40
    {
41
        $paths = [];
42
        if (defined('XOOPS_VAR_PATH') && is_string(XOOPS_VAR_PATH) && XOOPS_VAR_PATH !== '') {
0 ignored issues
show
introduced by
The condition Xoops\RegDom\XOOPS_VAR_PATH !== '' is always false.
Loading history...
43
            $paths[] = XOOPS_VAR_PATH . '/cache/regdom/psl.cache.php';
44
        }
45
        $paths[] = __DIR__ . '/../data/psl.cache.php';
46
47
        foreach ($paths as $path) {
48
            if (is_file($path) && is_readable($path)) {
49
                $rules = include $path;
50
51
                if (
52
                    is_array($rules)
53
                    && isset($rules['NORMAL'], $rules['WILDCARD'], $rules['EXCEPTION'])
54
                    && is_array($rules['NORMAL'])
55
                    && is_array($rules['WILDCARD'])
56
                ) {
57
                    $totalRules = count($rules['NORMAL']) + count($rules['WILDCARD']);
58
                    if ($totalRules > 1000 && $totalRules < 100000) {
59
                        /** @var array{'NORMAL': array<string, true>, 'WILDCARD': array<string, true>, 'EXCEPTION': array<string, true>} $rules */
60
                        return $rules;
61
                    }
62
                }
63
            }
64
        }
65
        // Last resort: throw an exception instead of logging
66
        throw new \Xoops\RegDom\Exception\PslCacheNotFoundException('No valid PSL cache found. Run `composer run update-psl` to generate one.');
67
    }
68
69
    /**
70
     * Checks if a given domain is a public suffix (e.g., 'com', 'co.uk').
71
     */
72
    public function isPublicSuffix(string $domain): bool
73
    {
74
        $domain = $this->normalizeDomain($domain);
75
        if ($domain === '' || filter_var($domain, FILTER_VALIDATE_IP)) {
76
            return false;
77
        }
78
79
        if (isset(self::$rules['EXCEPTION'][$domain])) {
80
            return false;
81
        }
82
        if (isset(self::$rules['NORMAL'][$domain])) {
83
            return true;
84
        }
85
86
        $parts = explode('.', $domain);
87
        if (count($parts) >= 2) {
88
            array_shift($parts);
89
            $parent = implode('.', $parts);
90
            if (isset(self::$rules['WILDCARD'][$parent])) {
91
                return true;
92
            }
93
        }
94
95
        return false;
96
    }
97
98
    /**
99
     * Gets the public suffix portion of a full domain.
100
     */
101
    public function getPublicSuffix(string $domain): ?string
102
    {
103
        $domain = $this->normalizeDomain($domain);
104
        if ($domain === '' || filter_var($domain, FILTER_VALIDATE_IP)) {
105
            return null;
106
        }
107
108
        $parts = explode('.', $domain);
109
        $n = count($parts);
110
        for ($i = 0; $i < $n; $i++) {
111
            $testSuffix = implode('.', array_slice($parts, $i));
112
            if (isset(self::$rules['EXCEPTION'][$testSuffix])) {
113
                return $i > 0 ? implode('.', array_slice($parts, $i - 1)) : null;
114
            }
115
            if (isset(self::$rules['NORMAL'][$testSuffix])) {
116
                return $testSuffix;
117
            }
118
            if ($i < $n - 1) {
119
                $parent = implode('.', array_slice($parts, $i + 1));
120
                if (isset(self::$rules['WILDCARD'][$parent])) {
121
                    return $testSuffix;
122
                }
123
            }
124
        }
125
        return null;
126
    }
127
128
    /**
129
     * Gets metadata about the loaded PSL cache, including a warning flag if the data is stale.
130
     *
131
     * @return array{
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{ at position 2 could not be parsed: the token is null at position 2.
Loading history...
132
     * active_cache: string|null,
133
     * last_updated: string|null,
134
     * days_old: int|null,
135
     * rule_counts: array<string, int>,
136
     * needs_update: bool,
137
     * error?: string
138
     * } Metadata about the active cache.
139
     */
140
    public function getMetadata(): array
141
    {
142
        // Add a guard clause to handle the case where rules are not loaded.
143
        if (self::$rules === null) {
144
            return [
145
                'active_cache' => null,
146
                'last_updated' => null,
147
                'days_old'     => null,
148
                'rule_counts'  => ['normal' => 0, 'wildcard' => 0, 'exception' => 0],
149
                'needs_update' => true,
150
                'error'        => 'Rules not loaded',
151
            ];
152
        }
153
154
        $runtimePath = null;
155
        // Add is_string() to ensure the constant is safe to use.
156
        if (defined('XOOPS_VAR_PATH') && is_string(XOOPS_VAR_PATH) && XOOPS_VAR_PATH !== '') {
0 ignored issues
show
introduced by
The condition Xoops\RegDom\XOOPS_VAR_PATH !== '' is always false.
Loading history...
157
            $runtimePath = XOOPS_VAR_PATH . '/cache/regdom/psl.cache.php';
158
        }
159
        $bundledPath = __DIR__ . '/../data/psl.cache.php';
160
161
        $activeCache = null;
162
        $lastUpdated = null;
163
164
        if ($runtimePath && file_exists($runtimePath)) {
0 ignored issues
show
Bug introduced by
$runtimePath of type void is incompatible with the type string expected by parameter $filename of file_exists(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

164
        if ($runtimePath && file_exists(/** @scrutinizer ignore-type */ $runtimePath)) {
Loading history...
introduced by
$runtimePath is of type null, thus it always evaluated to false.
Loading history...
165
            $activeCache = 'runtime';
166
            $lastUpdated = filemtime($runtimePath);
167
        } elseif (file_exists($bundledPath)) {
168
            $activeCache = 'bundled';
169
            $lastUpdated = filemtime($bundledPath);
170
        }
171
172
        // Cast the result of floor() to an integer to match the docblock.
173
        $daysOld = $lastUpdated ? (int) floor((time() - $lastUpdated) / 86400) : null;
174
175
        $metadata = [
176
            'active_cache' => $activeCache,
177
            'last_updated' => $lastUpdated ? date('Y-m-d H:i:s T', $lastUpdated) : null,
178
            'days_old'     => $daysOld,
179
            'rule_counts'  => [
180
                'normal'    => count(self::$rules['NORMAL']),
181
                'wildcard'  => count(self::$rules['WILDCARD']),
182
                'exception' => count(self::$rules['EXCEPTION']),
183
            ],
184
            'needs_update' => false,
185
        ];
186
187
        if ($metadata['days_old'] && $metadata['days_old'] > 180) {
188
            $metadata['needs_update'] = true;
189
        }
190
191
        return $metadata;
192
    }
193
194
    /**
195
     * Normalizes a domain string for consistent processing.
196
     */
197
    private function normalizeDomain(string $domain): string
198
    {
199
        $domain = strtolower(trim($domain));
200
        // Handles both leading and trailing dots
201
        $domain = trim($domain, '.');
202
203
        if (function_exists('idn_to_ascii')) {
204
            $domain = idn_to_ascii($domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46) ?: $domain;
205
        }
206
207
        return $domain;
208
    }
209
210
    /**
211
     * Checks if a domain is an explicit exception in the PSL.
212
     * @param string $domain The domain to check.
213
     * @return bool
214
     */
215
    public function isException(string $domain): bool
216
    {
217
        $domain = $this->normalizeDomain($domain);
218
        return isset(self::$rules['EXCEPTION'][$domain]);
219
    }
220
}
221