Passed
Pull Request — master (#582)
by Théo
02:12
created

Whitelist::create()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 43
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6.5163

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 28
c 1
b 0
f 0
nc 5
nop 6
dl 0
loc 43
ccs 17
cts 28
cp 0.6071
crap 6.5163
rs 9.1608
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the humbug/php-scoper package.
7
 *
8
 * Copyright (c) 2017 Théo FIDRY <[email protected]>,
9
 *                    Pádraic Brady <[email protected]>
10
 *
11
 * For the full copyright and license information, please view the LICENSE
12
 * file that was distributed with this source code.
13
 */
14
15
namespace Humbug\PhpScoper;
16
17
use Countable;
18
use Humbug\PhpScoper\Symbol\NamespaceRegistry;
19
use Humbug\PhpScoper\Symbol\SymbolsRegistry;
20
use InvalidArgumentException;
21
use PhpParser\Node\Name\FullyQualified;
22
use function array_key_exists;
23
use function array_map;
24
use function array_pop;
25
use function array_unique;
26
use function array_values;
27
use function count;
28
use function explode;
29
use function implode;
30
use function ltrim;
31
use function preg_match as native_preg_match;
32
use function Safe\array_flip;
33
use function Safe\sprintf;
34
use function Safe\substr;
35
use function str_replace;
36
use function strpos;
37
use function strtolower;
38
use function trim;
39
40
final class Whitelist implements Countable
41
{
42
    /**
43
     * @var list<string>
0 ignored issues
show
Bug introduced by
The type Humbug\PhpScoper\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
44
     */
45
    private array $originalElements;
46
47
    /**
48
     * @var array<string, mixed>
49
     */
50
    private array $exposedSymbols;
51
52 15
    /**
53
     * @var array<string, mixed>
54
     */
55
    private array $exposedConstants;
56
57
    /**
58 15
     * @var list<string>
59 15
     */
60 15
    private array $exposedSymbolsPatterns;
61 15
62 15
    private bool $exposeGlobalConstants;
63
    private bool $exposeGlobalClasses;
64 15
    private bool $exposeGlobalFunctions;
65 12
66 2
    private array $whitelistedFunctions = [];
67
    private array $whitelistedClasses = [];
68
69 12
    private NamespaceRegistry $excludedNamespaces;
70
71
    /**
72
     * @param string[] $excludedNamespaceRegexes
73
     * @param string[] $excludedNamespaceNames
74
     */
75
    public static function create(
76
        bool $exposeGlobalConstants = false,
77
        bool $exposeGlobalClasses = false,
78 12
        bool $exposeGlobalFunctions = false,
79
        array $excludedNamespaceRegexes = [],
80 12
        array $excludedNamespaceNames = [],
81 2
        string ...$exposedElements
82 11
    ): self {
83 3
        $exposedSymbols = [];
84 9
        $exposedConstants = [];
85
        $exposedSymbolsPatterns = [];
86
        $originalElements = [];
87
        $excludedNamespaceNames = array_map('strtolower', $excludedNamespaceNames);
88
89
        foreach ($exposedElements as $element) {
90
            $element = ltrim(trim($element), '\\');
91
92
            self::assertValidElement($element);
93
94
            $originalElements[] = $element;
95
96
            if ('\*' === substr($element, -2)) {
97
                $excludedNamespaceNames[] = strtolower(substr($element, 0, -2));
98
            } elseif ('*' === $element) {
99
                $excludedNamespaceNames[] = '';
100 9
            } elseif (false !== strpos($element, '*')) {
101 9
                $exposedSymbolsPatterns[] = self::createExposePattern($element);
102
            } else {
103
                $exposedSymbols[] = strtolower($element);
104
                $exposedConstants[] = self::lowerCaseConstantName($element);
105 15
            }
106 15
        }
107 15
108 15
        return new self(
109 15
            $exposeGlobalConstants,
110 15
            $exposeGlobalClasses,
111 15
            $exposeGlobalFunctions,
112 15
            array_unique($originalElements),
113 15
            array_flip($exposedSymbols),
114
            array_flip($exposedConstants),
115
            array_unique($exposedSymbolsPatterns),
116
            array_unique($excludedNamespaceNames),
117
            array_unique($excludedNamespaceRegexes),
118
        );
119
    }
120
121
    /**
122
     * @param list<string>       $originalElements
123
     * @param array<string, int> $exposedSymbols
124
     * @param array<string, int> $exposedConstants
125
     * @param list<string>       $exposedSymbolsPatterns
126
     * @param list<string>       $excludedNamespaceNames
127
     * @param list<string>       $excludedNamespaceRegexes
128
     */
129
    public function __construct(
130
        bool $exposeGlobalConstants,
131
        bool $exposeGlobalClasses,
132
        bool $exposeGlobalFunctions,
133
        array $originalElements,
134 15
        array $exposedSymbols,
135
        array $exposedConstants,
136
        array $exposedSymbolsPatterns,
137
        array $excludedNamespaceNames,
138
        array $excludedNamespaceRegexes
139
    ) {
140
        $this->exposeGlobalConstants = $exposeGlobalConstants;
141
        $this->exposeGlobalClasses = $exposeGlobalClasses;
142
        $this->exposeGlobalFunctions = $exposeGlobalFunctions;
143
        $this->originalElements = $originalElements;
0 ignored issues
show
Documentation Bug introduced by
It seems like $originalElements of type array is incompatible with the declared type Humbug\PhpScoper\list of property $originalElements.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
144 15
        $this->exposedSymbols = $exposedSymbols;
145 15
        $this->exposedConstants = $exposedConstants;
146 15
        $this->exposedSymbolsPatterns = $exposedSymbolsPatterns;
0 ignored issues
show
Documentation Bug introduced by
It seems like $exposedSymbolsPatterns of type array is incompatible with the declared type Humbug\PhpScoper\list of property $exposedSymbolsPatterns.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
147 15
        $this->excludedNamespaces = NamespaceRegistry::create(
148 15
            $excludedNamespaceRegexes,
149 15
            $excludedNamespaceNames,
150 15
        );
151 15
    }
152
153
    public function belongsToExcludedNamespace(string $name): bool
154 521
    {
155
        return $this->excludedNamespaces->belongsToRegisteredNamespace($name);
156 521
    }
157
158 521
    public function isExcludedNamespace(string $name): bool
159 41
    {
160 37
        return $this->excludedNamespaces->isRegisteredNamespace($name);
161
    }
162
163
    /**
164 492
     * @internal
165
     */
166
    public function exposeGlobalFunctions(): bool
167 561
    {
168
        return $this->exposeGlobalFunctions;
169 561
    }
170
171 561
    public function isExposedFunctionFromGlobalNamespace(string $functionName): bool
172
    {
173
        return $this->exposeGlobalFunctions && !strpos($functionName, '\\');
174
    }
175 561
176 41
    public function recordWhitelistedFunction(FullyQualified $original, FullyQualified $alias): void
177 20
    {
178
        $this->whitelistedFunctions[(string) $original] = [(string) $original, (string) $alias];
179
    }
180 21
181 9
    public function getRecordedWhitelistedFunctions(): array
182
    {
183
        return array_values($this->whitelistedFunctions);
184 17
    }
185 17
186
    /**
187 17
     * @internal
188 17
     */
189 2
    public function exposeGlobalConstants(): bool
190
    {
191
        return $this->exposeGlobalConstants;
192
    }
193 15
194
    public function isExposedConstantFromGlobalNamespace(string $constantName): bool
195
    {
196 529
        return $this->exposeGlobalConstants && !strpos($constantName, '\\');
197
    }
198
199
    /**
200
     * @internal
201
     */
202 551
    public function exposeGlobalClasses(): bool
203
    {
204 551
        return $this->exposeGlobalClasses;
205
    }
206
207 109
    public function isExposedClassFromGlobalNamespace(string $className): bool
208
    {
209 109
        return $this->exposeGlobalClasses && !strpos($className, '\\');
210
    }
211
212 24
    public function recordWhitelistedClass(FullyQualified $original, FullyQualified $alias): void
213
    {
214 24
        $this->whitelistedClasses[(string) $original] = [(string) $original, (string) $alias];
215
    }
216
217 555
    public function getRecordedWhitelistedClasses(): array
218
    {
219 555
        return array_values($this->whitelistedClasses);
220
    }
221
222
    /**
223
     * Tells if a given symbol is exposed. Note however that it does not account for when:
224
     *
225 551
     * - The symbol belongs to the global namespace and the symbols of the global namespace of this type are exposed
226
     * - Belongs to an excluded namespace
227 551
     *
228
     * @param bool $constant Unlike other symbols, constants _can_ be case insensitive but 99% are not so we leave out
229
     *                       the case where they are not case sensitive.
230 81
     */
231
    public function isSymbolExposed(string $name, bool $constant = false): bool
232 81
    {
233
        if (!$constant && array_key_exists(strtolower($name), $this->exposedSymbols)) {
234
            return true;
235
        }
236
237
        if ($constant && array_key_exists(self::lowerCaseConstantName($name), $this->exposedConstants)) {
238 6
            return true;
239
        }
240 6
241
        foreach ($this->exposedSymbolsPatterns as $pattern) {
242
            $pattern = !$constant ? $pattern.'i' : $pattern;
243 324
244
            if (1 === native_preg_match($pattern, $name)) {
245 324
                return true;
246
            }
247
        }
248 82
249
        return false;
250 82
    }
251
252
    /**
253 555
     * @return string[]
254
     */
255 555
    public function toArray(): array
256
    {
257
        return $this->originalElements;
258
    }
259
260
    public function count(): int
261
    {
262
        return count($this->whitelistedFunctions) + count($this->whitelistedClasses);
263
    }
264
265
    public function registerFromRegistry(SymbolsRegistry $registry): void
266
    {
267 442
        foreach ($registry->getRecordedClasses() as [$original, $alias]) {
268
            $this->recordWhitelistedClass(
269 442
                new FullyQualified($original),
270 94
                new FullyQualified($alias),
271
            );
272
        }
273 400
274 24
        foreach ($registry->getRecordedFunctions() as [$original, $alias]) {
275
            $this->recordWhitelistedFunction(
276
                new FullyQualified($original),
277 385
                new FullyQualified($alias),
278 18
            );
279
        }
280 18
    }
281 11
282
    private static function assertValidElement(string $element): void
283
    {
284
        if ('' !== $element) {
285 378
            return;
286
        }
287
288
        throw new InvalidArgumentException(
289
            sprintf(
290
                'Invalid whitelist element "%s": cannot accept an empty string',
291
                $element,
292
            ),
293
        );
294
    }
295
296
    private static function createExposePattern(string $element): string
297
    {
298
        self::assertValidPattern($element);
299
300
        return sprintf(
301
            '/^%s$/u',
302
            str_replace(
303 554
                '\\',
304
                '\\\\',
305 554
                str_replace(
306
                    '*',
307
                    '.*',
308
                    $element,
309
                ),
310
            ),
311
        );
312
    }
313
314
    private static function assertValidPattern(string $element): void
315
    {
316
        if (1 !== native_preg_match('/^(([\p{L}_]+\\\\)+)?[\p{L}_]*\*$/u', $element)) {
317
            throw new InvalidArgumentException(sprintf('Invalid whitelist pattern "%s".', $element));
318
        }
319
    }
320 124
321
    /**
322 124
     * Transforms the constant FQ name "Acme\Foo\X" to "acme\foo\X" since the namespace remains case insensitive for
323
     * constants regardless of whether or not constants actually are case insensitive.
324 124
     */
325
    private static function lowerCaseConstantName(string $name): string
326 124
    {
327
        $parts = explode('\\', $name);
328 124
329
        $lastPart = array_pop($parts);
330 124
331
        $parts = array_map('strtolower', $parts);
332
333 521
        $parts[] = $lastPart;
334
335 521
        return implode('\\', $parts);
336
    }
337
}
338