Passed
Pull Request — master (#519)
by Théo
02:25
created

Whitelist::isSymbolWhitelisted()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 8

Importance

Changes 0
Metric Value
cc 8
eloc 9
nc 7
nop 2
dl 0
loc 19
ccs 5
cts 5
cp 1
crap 8
rs 8.4444
c 0
b 0
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A Whitelist::toArray() 0 3 1
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 InvalidArgumentException;
19
use PhpParser\Node\Name\FullyQualified;
20
use function array_filter;
21
use function array_key_exists;
22
use function array_map;
23
use function array_pop;
24
use function array_unique;
25
use function array_values;
26
use function count;
27
use function explode;
28
use function implode;
29
use function ltrim;
30
use function preg_match as native_preg_match;
31
use function Safe\array_flip;
32
use function Safe\sprintf;
33
use function Safe\substr;
34
use function str_replace;
35
use function strpos;
36
use function strtolower;
37
use function trim;
38
39
final class Whitelist implements Countable
40
{
41
    /**
42
     * @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...
43
     */
44
    private array $originalElements;
45
46
    /**
47
     * @var array<string, mixed>
48
     */
49
    private array $exposedSymbols;
50
51
    /**
52 15
     * @var array<string, mixed>
53
     */
54
    private array $exposedConstants;
55
56
    /**
57
     * @var list<string>
58 15
     */
59 15
    private array $excludedNamespaceNames;
60 15
61 15
    /**
62 15
     * @var list<string>
63
     */
64 15
    private array $exposedSymbolsPatterns;
65 12
66 2
    private bool $exposeGlobalConstants;
67
    private bool $exposeGlobalClasses;
68
    private bool $exposeGlobalFunctions;
69 12
70
    private array $whitelistedFunctions = [];
71
    private array $whitelistedClasses = [];
72
73
    public static function create(
74
        bool $exposeGlobalConstants,
75
        bool $exposeGlobalClasses,
76
        bool $exposeGlobalFunctions,
77
        string ...$exposedElements
78 12
    ): self {
79
        $exposedSymbols = [];
80 12
        $exposedConstants = [];
81 2
        $exposedNamespaceNames = [];
82 11
        $exposedSymbolsPatterns = [];
83 3
        $originalElements = [];
84 9
85
        foreach ($exposedElements as $element) {
86
            $element = ltrim(trim($element), '\\');
87
88
            self::assertValidElement($element);
89
90
            $originalElements[] = $element;
91
92
            if ('\*' === substr($element, -2)) {
93
                $exposedNamespaceNames[] = strtolower(substr($element, 0, -2));
94
            } elseif ('*' === $element) {
95
                $exposedNamespaceNames[] = '';
96
            } elseif (false !== strpos($element, '*')) {
97
                $exposedSymbolsPatterns[] = self::createExposePattern($element);
98
            } else {
99
                $exposedSymbols[] = strtolower($element);
100 9
                $exposedConstants[] = self::lowerCaseConstantName($element);
101 9
            }
102
        }
103
104
        return new self(
105 15
            $exposeGlobalConstants,
106 15
            $exposeGlobalClasses,
107 15
            $exposeGlobalFunctions,
108 15
            array_unique($originalElements),
109 15
            array_flip($exposedSymbols),
110 15
            array_flip($exposedConstants),
111 15
            array_unique($exposedSymbolsPatterns),
112 15
            array_unique($exposedNamespaceNames)
113 15
        );
114
    }
115
116
    /**
117
     * @param list<string>       $originalElements
118
     * @param array<string, int> $exposedSymbols
119
     * @param array<string, int> $exposedConstants
120
     * @param list<string>       $exposedSymbolsPatterns
121
     * @param list<string>       $excludedNamespaceNames
122
     */
123
    public function __construct(
124
        bool $exposeGlobalConstants,
125
        bool $exposeGlobalClasses,
126
        bool $exposeGlobalFunctions,
127
        array $originalElements,
128
        array $exposedSymbols,
129
        array $exposedConstants,
130
        array $exposedSymbolsPatterns,
131
        array $excludedNamespaceNames
132
    ) {
133
        $this->exposeGlobalConstants = $exposeGlobalConstants;
134 15
        $this->exposeGlobalClasses = $exposeGlobalClasses;
135
        $this->exposeGlobalFunctions = $exposeGlobalFunctions;
136
        $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...
137
        $this->exposedSymbols = $exposedSymbols;
138
        $this->exposedConstants = $exposedConstants;
139
        $this->excludedNamespaceNames = $excludedNamespaceNames;
0 ignored issues
show
Documentation Bug introduced by
It seems like $excludedNamespaceNames of type array is incompatible with the declared type Humbug\PhpScoper\list of property $excludedNamespaceNames.

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