, * Pádraic Brady * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Humbug\PhpScoper; use Countable; use InvalidArgumentException; use PhpParser\Node\Name\FullyQualified; use function array_filter; use function array_flip; use function array_key_exists; use function array_map; use function array_pop; use function array_unique; use function array_values; use function count; use function explode; use function implode; use function preg_match; use function sprintf; use function str_replace; use function strpos; use function strtolower; use function substr; use function trim; final class Whitelist implements Countable { private $original; private $symbols; private $constants; private $namespaces; private $patterns; private $whitelistGlobalConstants; private $whitelistGlobalClasses; private $whitelistGlobalFunctions; private $whitelistedFunctions = []; private $whitelistedClasses = []; public static function create( bool $whitelistGlobalConstants, bool $whitelistGlobalClasses, bool $whitelistGlobalFunctions, string ...$elements ): self { $symbols = []; $constants = []; $namespaces = []; $patterns = []; $original = []; foreach ($elements as $element) { if (isset($element[0]) && '\\' === $element[0]) { $element = substr($element, 1); } if ('' === trim($element)) { throw new InvalidArgumentException(sprintf('Invalid whitelist element "%s": cannot accept an empty string', $element)); } $original[] = $element; if ('\*' === substr($element, -2)) { $namespaces[] = strtolower(substr($element, 0, -2)); } elseif ('*' === $element) { $namespaces[] = ''; } elseif (false !== strpos($element, '*')) { self::assertValidPattern($element); $patterns[] = sprintf( '/^%s$/u', str_replace( '\\', '\\\\', str_replace( '*', '.*', $element ) ) ); } else { $symbols[] = strtolower($element); $constants[] = self::lowerConstantName($element); } } return new self( $whitelistGlobalConstants, $whitelistGlobalClasses, $whitelistGlobalFunctions, array_unique($original), array_flip($symbols), array_flip($constants), array_unique($patterns), array_unique($namespaces) ); } private static function assertValidPattern(string $element): void { if (1 !== preg_match('/^(([\p{L}_]+\\\\)+)?[\p{L}_]*\*$/u', $element)) { throw new InvalidArgumentException(sprintf('Invalid whitelist pattern "%s".', $element)); } } /** * @param string[] $original * @param string[] $patterns * @param string[] $namespaces */ private function __construct( bool $whitelistGlobalConstants, bool $whitelistGlobalClasses, bool $whitelistGlobalFunctions, array $original, array $symbols, array $constants, array $patterns, array $namespaces ) { $this->whitelistGlobalConstants = $whitelistGlobalConstants; $this->whitelistGlobalClasses = $whitelistGlobalClasses; $this->whitelistGlobalFunctions = $whitelistGlobalFunctions; $this->original = $original; $this->symbols = $symbols; $this->constants = $constants; $this->namespaces = $namespaces; $this->patterns = $patterns; } public function belongsToWhitelistedNamespace(string $name): bool { $nameNamespace = $this->retrieveNameNamespace($name); foreach ($this->namespaces as $namespace) { if ('' === $namespace || 0 === strpos($nameNamespace, $namespace)) { return true; } } return false; } public function isWhitelistedNamespace(string $name): bool { $name = strtolower($name); if (0 === strpos($name, '\\')) { $name = substr($name, 1); } foreach ($this->namespaces as $namespace) { if ('' === $namespace) { return true; } if ('' !== $namespace && 0 !== strpos($name, $namespace)) { continue; } $nameParts = explode('\\', $name); foreach (explode('\\', $namespace) as $index => $namespacePart) { if ($nameParts[$index] !== $namespacePart) { return false; } } return true; } return false; } /** * @internal */ public function whitelistGlobalFunctions(): bool { return $this->whitelistGlobalFunctions; } public function isGlobalWhitelistedFunction(string $functionName): bool { return $this->whitelistGlobalFunctions && false === strpos($functionName, '\\'); } public function recordWhitelistedFunction(FullyQualified $original, FullyQualified $alias): void { $this->whitelistedFunctions[(string) $original] = [(string) $original, (string) $alias]; } public function getRecordedWhitelistedFunctions(): array { return array_values($this->whitelistedFunctions); } /** * @internal */ public function whitelistGlobalConstants(): bool { return $this->whitelistGlobalConstants; } public function isGlobalWhitelistedConstant(string $constantName): bool { return $this->whitelistGlobalConstants && false === strpos($constantName, '\\'); } /** * @internal */ public function whitelistGlobalClasses(): bool { return $this->whitelistGlobalClasses; } public function isGlobalWhitelistedClass(string $className): bool { return $this->whitelistGlobalClasses && false === strpos($className, '\\'); } public function recordWhitelistedClass(FullyQualified $original, FullyQualified $alias): void { $this->whitelistedClasses[(string) $original] = [(string) $original, (string) $alias]; } public function getRecordedWhitelistedClasses(): array { return array_values($this->whitelistedClasses); } /** * Tells if a given symbol is whitelisted. Note however that it does not account for when:. * * - The symbol belongs to the global namespace and the symbols of the global namespace of this type are whitelisted * - Belongs to a whitelisted namespace * * @param bool $constant Unlike other symbols, constants _can_ be case insensitive but 99% are not so we leave out * the case where they are not case sensitive. */ public function isSymbolWhitelisted(string $name, bool $constant = false): bool { if (false === $constant && array_key_exists(strtolower($name), $this->symbols)) { return true; } if ($constant && array_key_exists(self::lowerConstantName($name), $this->constants)) { return true; } foreach ($this->patterns as $pattern) { $pattern = false === $constant ? $pattern.'i' : $pattern; if (1 === preg_match($pattern, $name)) { return true; } } return false; } /** * @return string[] * * @deprecated To be replaced by getWhitelistedClasses */ public function getClassWhitelistArray(): array { return array_filter( $this->original, static function (string $name): bool { return '*' !== $name && '\*' !== substr($name, -2); } ); } public function toArray(): array { return $this->original; } /** * {@inheritdoc} */ public function count(): int { return count($this->whitelistedFunctions) + count($this->whitelistedClasses); } /** * Transforms the constant FQ name "Acme\Foo\X" to "acme\foo\X" since the namespace remains case insensitive for * constants regardless of whether or not constants actually are case insensitive. */ private static function lowerConstantName(string $name): string { $parts = explode('\\', $name); $lastPart = array_pop($parts); $parts = array_map('strtolower', $parts); $parts[] = $lastPart; return implode('\\', $parts); } private function retrieveNameNamespace(string $name): string { $name = strtolower($name); if (0 === strpos($name, '\\')) { $name = substr($name, 1); } $nameParts = explode('\\', $name); array_pop($nameParts); return [] === $nameParts ? '' : implode('\\', $nameParts); } }