Passed
Pull Request — master (#517)
by Théo
04:59 queued 03:01
created

Whitelist::lowerConstantName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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