Passed
Push — master ( 40b894...ddcf42 )
by Théo
02:15
created

Whitelist::retrieveNameNamespace()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
nc 4
nop 1
dl 0
loc 14
ccs 7
cts 7
cp 1
crap 3
rs 9.7998
c 0
b 0
f 0
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_flip;
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 preg_match;
30
use function sprintf;
31
use function str_replace;
32
use function strpos;
33
use function strtolower;
34
use function substr;
35
use function trim;
36
37
final class Whitelist implements Countable
38
{
39
    private $original;
40
    private $symbols;
41
    private $constants;
42
    private $namespaces;
43
    private $patterns;
44
45
    private $whitelistGlobalConstants;
46
    private $whitelistGlobalClasses;
47
    private $whitelistGlobalFunctions;
48
49
    private $whitelistedFunctions = [];
50
    private $whitelistedClasses = [];
51
52 15
    public static function create(
53
        bool $whitelistGlobalConstants,
54
        bool $whitelistGlobalClasses,
55
        bool $whitelistGlobalFunctions,
56
        string ...$elements
57
    ): self {
58 15
        $symbols = [];
59 15
        $constants = [];
60 15
        $namespaces = [];
61 15
        $patterns = [];
62 15
        $original = [];
63
64 15
        foreach ($elements as $element) {
65 12
            if (isset($element[0]) && '\\' === $element[0]) {
66 2
                $element = substr($element, 1);
67
            }
68
69 12
            if ('' === trim($element)) {
70
                throw new InvalidArgumentException(
71
                    sprintf(
72
                        'Invalid whitelist element "%s": cannot accept an empty string',
73
                        $element
74
                    )
75
                );
76
            }
77
78 12
            $original[] = $element;
79
80 12
            if ('\*' === substr($element, -2)) {
81 2
                $namespaces[] = strtolower(substr($element, 0, -2));
82 11
            } elseif ('*' === $element) {
83 3
                $namespaces[] = '';
84 9
            } elseif (false !== strpos($element, '*')) {
85
                self::assertValidPattern($element);
86
87
                $patterns[] = sprintf(
88
                    '/^%s$/u',
89
                    str_replace(
90
                        '\\',
91
                        '\\\\',
92
                        str_replace(
93
                            '*',
94
                            '.*',
95
                            $element
96
                        )
97
                    )
98
                );
99
            } else {
100 9
                $symbols[] = strtolower($element);
101 12
                $constants[] = self::lowerConstantName($element);
102
            }
103
        }
104
105 15
        return new self(
106 15
            $whitelistGlobalConstants,
107 15
            $whitelistGlobalClasses,
108 15
            $whitelistGlobalFunctions,
109 15
            array_unique($original),
110 15
            array_flip($symbols),
111 15
            array_flip($constants),
112 15
            array_unique($patterns),
113 15
            array_unique($namespaces)
114
        );
115
    }
116
117
    private static function assertValidPattern(string $element): void
118
    {
119
        if (1 !== preg_match('/^(([\p{L}_]+\\\\)+)?[\p{L}_]*\*$/u', $element)) {
120
            throw new InvalidArgumentException(
121
                sprintf(
122
                    'Invalid whitelist pattern "%s".',
123
                    $element
124
                )
125
            );
126
        }
127
    }
128
129
    /**
130
     * @param string[] $original
131
     * @param string[] $patterns
132
     * @param string[] $namespaces
133
     */
134 15
    private function __construct(
135
        bool $whitelistGlobalConstants,
136
        bool $whitelistGlobalClasses,
137
        bool $whitelistGlobalFunctions,
138
        array $original,
139
        array $symbols,
140
        array $constants,
141
        array $patterns,
142
        array $namespaces
143
    ) {
144 15
        $this->whitelistGlobalConstants = $whitelistGlobalConstants;
145 15
        $this->whitelistGlobalClasses = $whitelistGlobalClasses;
146 15
        $this->whitelistGlobalFunctions = $whitelistGlobalFunctions;
147 15
        $this->original = $original;
148 15
        $this->symbols = $symbols;
149 15
        $this->constants = $constants;
150 15
        $this->namespaces = $namespaces;
151 15
        $this->patterns = $patterns;
152
    }
153
154 520
    public function belongsToWhitelistedNamespace(string $name): bool
155
    {
156 520
        $nameNamespace = $this->retrieveNameNamespace($name);
157
158 520
        foreach ($this->namespaces as $namespace) {
159 41
            if ('' === $namespace || 0 === strpos($nameNamespace, $namespace)) {
160 41
                return true;
161
            }
162
        }
163
164 491
        return false;
165
    }
166
167 560
    public function isWhitelistedNamespace(string $name): bool
168
    {
169 560
        $name = strtolower($name);
170
171 560
        if (0 === strpos($name, '\\')) {
172
            $name = substr($name, 1);
173
        }
174
175 560
        foreach ($this->namespaces as $namespace) {
176 41
            if ('' === $namespace) {
177 20
                return true;
178
            }
179
180 21
            if ('' !== $namespace && 0 !== strpos($name, $namespace)) {
181 9
                continue;
182
            }
183
184 17
            $nameParts = explode('\\', $name);
185 17
            $namespaceParts = explode('\\', $namespace);
186
187 17
            foreach ($namespaceParts as $index => $namespacePart) {
188 17
                if ($nameParts[$index] !== $namespacePart) {
189 17
                    return false;
190
                }
191
            }
192
193 15
            return true;
194
        }
195
196 528
        return false;
197
    }
198
199
    /**
200
     * @internal
201
     */
202 550
    public function whitelistGlobalFunctions(): bool
203
    {
204 550
        return $this->whitelistGlobalFunctions;
205
    }
206
207 109
    public function isGlobalWhitelistedFunction(string $functionName): bool
208
    {
209 109
        return $this->whitelistGlobalFunctions && false === strpos($functionName, '\\');
210
    }
211
212 24
    public function recordWhitelistedFunction(FullyQualified $original, FullyQualified $alias): void
213
    {
214 24
        $this->whitelistedFunctions[(string) $original] = [(string) $original, (string) $alias];
215
    }
216
217 554
    public function getRecordedWhitelistedFunctions(): array
218
    {
219 554
        return array_values($this->whitelistedFunctions);
220
    }
221
222
    /**
223
     * @internal
224
     */
225 550
    public function whitelistGlobalConstants(): bool
226
    {
227 550
        return $this->whitelistGlobalConstants;
228
    }
229
230 81
    public function isGlobalWhitelistedConstant(string $constantName): bool
231
    {
232 81
        return $this->whitelistGlobalConstants && false === strpos($constantName, '\\');
233
    }
234
235
    /**
236
     * @internal
237
     */
238 6
    public function whitelistGlobalClasses(): bool
239
    {
240 6
        return $this->whitelistGlobalClasses;
241
    }
242
243 324
    public function isGlobalWhitelistedClass(string $className): bool
244
    {
245 324
        return $this->whitelistGlobalClasses && false === strpos($className, '\\');
246
    }
247
248 82
    public function recordWhitelistedClass(FullyQualified $original, FullyQualified $alias): void
249
    {
250 82
        $this->whitelistedClasses[(string) $original] = [(string) $original, (string) $alias];
251
    }
252
253 554
    public function getRecordedWhitelistedClasses(): array
254
    {
255 554
        return array_values($this->whitelistedClasses);
256
    }
257
258
    /**
259
     * Tells if a given symbol is whitelisted. Note however that it does not account for when:.
260
     *
261
     * - The symbol belongs to the global namespace and the symbols of the global namespace of this type are whitelisted
262
     * - Belongs to a whitelisted namespace
263
     *
264
     * @param bool $constant Unlike other symbols, constants _can_ be case insensitive but 99% are not so we leave out
265
     *                       the case where they are not case sensitive.
266
     */
267 442
    public function isSymbolWhitelisted(string $name, bool $constant = false): bool
268
    {
269 442
        if (false === $constant && array_key_exists(strtolower($name), $this->symbols)) {
270 94
            return true;
271
        }
272
273 400
        if ($constant && array_key_exists(self::lowerConstantName($name), $this->constants)) {
274 24
            return true;
275
        }
276
277 385
        foreach ($this->patterns as $pattern) {
278 18
            $pattern = false === $constant ? $pattern.'i' : $pattern;
279
280 18
            if (1 === preg_match($pattern, $name)) {
281 18
                return true;
282
            }
283
        }
284
285 378
        return false;
286
    }
287
288
    /**
289
     * @return string[]
290
     *
291
     * @deprecated To be replaced by getWhitelistedClasses
292
     */
293
    public function getClassWhitelistArray(): array
294
    {
295
        return array_filter(
296
            $this->original,
297
            static function (string $name): bool {
298
                return '*' !== $name && '\*' !== substr($name, -2);
299
            }
300
        );
301
    }
302
303 553
    public function toArray(): array
304
    {
305 553
        return $this->original;
306
    }
307
308
    /**
309
     * {@inheritdoc}
310
     */
311
    public function count(): int
312
    {
313
        return count($this->whitelistedFunctions) + count($this->whitelistedClasses);
314
    }
315
316
    /**
317
     * Transforms the constant FQ name "Acme\Foo\X" to "acme\foo\X" since the namespace remains case insensitive for
318
     * constants regardless of whether or not constants actually are case insensitive.
319
     */
320 124
    private static function lowerConstantName(string $name): string
321
    {
322 124
        $parts = explode('\\', $name);
323
324 124
        $lastPart = array_pop($parts);
325
326 124
        $parts = array_map('strtolower', $parts);
327
328 124
        $parts[] = $lastPart;
329
330 124
        return implode('\\', $parts);
331
    }
332
333 520
    private function retrieveNameNamespace(string $name): string
334
    {
335 520
        $name = strtolower($name);
336
337 520
        if (0 === strpos($name, '\\')) {
338 22
            $name = substr($name, 1);
339
        }
340
341 520
        $nameParts = explode('\\', $name);
342
343 520
        array_pop($nameParts);
344
345 520
        return [] === $nameParts ? '' : implode('\\', $nameParts);
346
    }
347
}
348