Passed
Push — master ( 85c65e...8d63d9 )
by Théo
02:46
created

Whitelist::isGlobalWhitelistedFunction()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

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