Test Failed
Push — master ( 53494e...bc3061 )
by Théo
02:45
created

Whitelist::belongsToWhitelistedNamespace()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 6
nop 1
dl 0
loc 16
rs 9.4222
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 $whitelistGlobalFunctions;
48
49
    private $whitelistedFunctions = [];
50
    private $whitelistedClasses = [];
51
52
    public static function create(bool $whitelistGlobalConstants, bool $whitelistGlobalFunctions, string ...$elements): self
53
    {
54
        $symbols = [];
55
        $constants = [];
56
        $namespaces = [];
57
        $patterns = [];
58
        $original = [];
59
60
        foreach ($elements as $element) {
61
            if (isset($element[0]) && '\\' === $element[0]) {
62
                $element = substr($element, 1);
63
            }
64
65
            if ('' === trim($element)) {
66
                throw new InvalidArgumentException(
67
                    sprintf(
68
                        'Invalid whitelist element "%s": cannot accept an empty string',
69
                        $element
70
                    )
71
                );
72
            }
73
74
            $original[] = $element;
75
76
            if ('\*' === substr($element, -2)) {
77
                $namespaces[] = strtolower(substr($element, 0, -2));
78
            } elseif ('*' === $element) {
79
                $namespaces[] = '';
80
            } elseif (false !== strpos($element, '*')) {
81
                self::assertValidPattern($element);
82
83
                $patterns[] = sprintf(
84
                    '/^%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
101
        return new self(
102
            $whitelistGlobalConstants,
103
            $whitelistGlobalFunctions,
104
            array_unique($original),
105
            array_flip($symbols),
106
            array_flip($constants),
107
            array_unique($patterns),
108
            array_unique($namespaces)
109
        );
110
    }
111
112
    private static function assertValidPattern(string $element): void
113
    {
114
        if (1 !== preg_match('/^(([\p{L}_]+\\\\)+)?[\p{L}_]*\*$/u', $element)) {
115
            throw new \InvalidArgumentException(
116
                sprintf(
117
                    'Invalid whitelist pattern "%s".',
118
                    $element
119
                )
120
            );
121
        }
122
    }
123
124
    /**
125
     * @param string[] $original
126
     * @param string[] $patterns
127
     * @param string[] $namespaces
128
     */
129
    private function __construct(
130
        bool $whitelistGlobalConstants,
131
        bool $whitelistGlobalFunctions,
132
        array $original,
133
        array $symbols,
134
        array $constants,
135
        array $patterns,
136
        array $namespaces
137
    ) {
138
        $this->whitelistGlobalConstants = $whitelistGlobalConstants;
139
        $this->whitelistGlobalFunctions = $whitelistGlobalFunctions;
140
        $this->original = $original;
141
        $this->symbols = $symbols;
142
        $this->constants = $constants;
143
        $this->namespaces = $namespaces;
144
        $this->patterns = $patterns;
145
    }
146
147
    public function belongsToWhitelistedNamespace(string $name): bool
148
    {
149
        $name = strtolower($name);
150
151
        if (0 === strpos($name, '\\')) {
152
            $name = substr($name, 1);
153
        }
154
155
        foreach ($this->namespaces as $namespace) {
156
            if ('' === $namespace || 0 === strpos($name, $namespace)) {
157
                return true;
158
            }
159
        }
160
161
        return false;
162
    }
163
164
    /**
165
     * @internal
166
     */
167
    public function whitelistGlobalFunctions(): bool
168
    {
169
        return $this->whitelistGlobalFunctions;
170
    }
171
172
    public function isGlobalWhitelistedFunction(string $functionName): bool
173
    {
174
        return $this->whitelistGlobalFunctions && false === strpos($functionName, '\\');
175
    }
176
177
    public function recordWhitelistedFunction(FullyQualified $original, FullyQualified $alias): void
178
    {
179
        $this->whitelistedFunctions[] = [(string) $original, (string) $alias];
180
    }
181
182
    public function getWhitelistedFunctions(): array
183
    {
184
        return array_values(
185
            array_unique(
186
                $this->whitelistedFunctions,
187
                SORT_REGULAR
188
            )
189
        );
190
    }
191
192
    /**
193
     * @internal
194
     */
195
    public function whitelistGlobalConstants(): bool
196
    {
197
        return $this->whitelistGlobalConstants;
198
    }
199
200
    public function isGlobalWhitelistedConstant(string $constantName): bool
201
    {
202
        return $this->whitelistGlobalConstants && false === strpos($constantName, '\\');
203
    }
204
205
    /**
206
     * @internal
207
     */
208
    public function whitelistGlobalClasses(): bool
209
    {
210
        return $this->whitelistGlobalFunctions;
211
    }
212
213
    public function isGlobalWhitelistedClass(string $className): bool
0 ignored issues
show
Unused Code introduced by
The parameter $className is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
214
    {
215
        return false;
216
    }
217
218
    public function recordWhitelistedClass(FullyQualified $original, FullyQualified $alias): void
219
    {
220
        $this->whitelistedClasses[] = [(string) $original, (string) $alias];
221
    }
222
223
    public function getWhitelistedClasses(): array
224
    {
225
        return $this->whitelistedClasses;
226
    }
227
228
    /**
229
     * Tells if a given symbol is whitelisted. Note however that it does not account for when:.
230
     *
231
     * - The symbol belongs to the global namespace and the symbols of the global namespace of this type are whitelisted
232
     * - Belongs to a whitelisted namespace
233
     *
234
     * @param bool $constant Unlike other symbols, constants _can_ be case insensitive but 99% are not so we leave out
235
     *                       the case where they are not case sensitive.
236
     */
237
    public function isSymbolWhitelisted(string $name, bool $constant = false): bool
238
    {
239
        if (false === $constant && array_key_exists(strtolower($name), $this->symbols)) {
240
            return true;
241
        }
242
243
        if ($constant && array_key_exists(self::lowerConstantName($name), $this->constants)) {
244
            return true;
245
        }
246
247
        foreach ($this->patterns as $pattern) {
248
            $pattern = false === $constant ? $pattern.'i' : $pattern;
249
250
            if (1 === preg_match($pattern, $name)) {
251
                return true;
252
            }
253
        }
254
255
        return false;
256
    }
257
258
    /**
259
     * @return string[]
260
     *
261
     * @deprecated To be replaced by getWhitelistedClasses
262
     */
263
    public function getClassWhitelistArray(): array
264
    {
265
        return array_filter(
266
            $this->original,
267
            function (string $name): bool {
268
                return '*' !== $name && '\*' !== substr($name, -2);
269
            }
270
        );
271
    }
272
273
    public function toArray(): array
274
    {
275
        return $this->original;
276
    }
277
278
    /**
279
     * {@inheritdoc}
280
     */
281
    public function count(): int
282
    {
283
        return count($this->whitelistedFunctions) + count($this->whitelistedClasses);
284
    }
285
286
    /**
287
     * Transforms the constant FQ name "Acme\Foo\X" to "acme\foo\X" since the namespace remains case insensitive for
288
     * constants regardless of whether or not constants actually are case insensitive.
289
     */
290
    private static function lowerConstantName(string $name): string
291
    {
292
        $parts = explode('\\', $name);
293
294
        $lastPart = array_pop($parts);
295
296
        $parts = array_map('strtolower', $parts);
297
298
        $parts[] = $lastPart;
299
300
        return implode('\\', $parts);
301
    }
302
}
303