, * 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 Humbug\PhpScoper\Patcher\SymfonyPatcher; use InvalidArgumentException; use Iterator; use RuntimeException; use SplFileInfo; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; use function array_filter; use function array_key_exists; use function array_keys; use function array_map; use function array_merge; use function array_reduce; use function array_unique; use function array_unshift; use function dirname; use function file_exists; use function file_get_contents; use function gettype; use function in_array; use function is_array; use function is_bool; use function is_callable; use function is_dir; use function is_file; use function is_link; use function is_readable; use function is_string; use function iterator_to_array; use function readlink; use function realpath; use function sprintf; use function trim; use const DIRECTORY_SEPARATOR; final class Configuration { private const PREFIX_KEYWORD = 'prefix'; private const WHITELISTED_FILES_KEYWORD = 'files-whitelist'; private const FINDER_KEYWORD = 'finders'; private const PATCHERS_KEYWORD = 'patchers'; private const WHITELIST_KEYWORD = 'whitelist'; private const WHITELIST_GLOBAL_CONSTANTS_KEYWORD = 'whitelist-global-constants'; private const WHITELIST_GLOBAL_CLASSES_KEYWORD = 'whitelist-global-classes'; private const WHITELIST_GLOBAL_FUNCTIONS_KEYWORD = 'whitelist-global-functions'; private const KEYWORDS = [ self::PREFIX_KEYWORD, self::WHITELISTED_FILES_KEYWORD, self::FINDER_KEYWORD, self::PATCHERS_KEYWORD, self::WHITELIST_KEYWORD, self::WHITELIST_GLOBAL_CONSTANTS_KEYWORD, self::WHITELIST_GLOBAL_CLASSES_KEYWORD, self::WHITELIST_GLOBAL_FUNCTIONS_KEYWORD, ]; private $path; private $prefix; private $filesWithContents; private $patchers; private $whitelist; private $whitelistedFiles; /** * @param string|null $path Absolute path to the configuration file. * @param string[] $paths List of paths to append besides the one configured */ public static function load(string $path = null, array $paths = []): self { if (null === $path) { $config = []; } else { if (false === (new Filesystem())->isAbsolutePath($path)) { throw new InvalidArgumentException( sprintf( 'Expected the path of the configuration file to load to be an absolute path, got "%s" ' .'instead', $path ) ); } if (false === file_exists($path)) { throw new InvalidArgumentException( sprintf( 'Expected the path of the configuration file to exists but the file "%s" could not be ' .'found', $path ) ); } if (false === is_file($path) && false === (is_link($path) && false !== readlink($path) && is_file(readlink($path)))) { throw new InvalidArgumentException( sprintf( 'Expected the path of the configuration file to be a file but "%s" appears to be a ' .'directory.', $path ) ); } $config = include $path; if (false === is_array($config)) { throw new InvalidArgumentException( sprintf( 'Expected configuration to be an array, found "%s" instead.', gettype($config) ) ); } } self::validateConfigKeys($config); $prefix = self::retrievePrefix($config); $whitelistedFiles = null === $path ? [] : self::retrieveWhitelistedFiles(dirname($path), $config); $patchers = self::retrievePatchers($config); array_unshift($patchers, new SymfonyPatcher()); $whitelist = self::retrieveWhitelist($config); $finders = self::retrieveFinders($config); $filesFromPaths = self::retrieveFilesFromPaths($paths); $filesWithContents = self::retrieveFilesWithContents(chain($filesFromPaths, ...$finders)); return new self($path, $prefix, $filesWithContents, $patchers, $whitelist, $whitelistedFiles); } /** * @param string|null $path Absolute path to the configuration file loaded. * @param string|null $prefix The prefix applied. * @param string[][] $filesWithContents Array of tuple with the first argument being the file path and the second its contents * @param callable[] $patchers List of closures which can alter the content of the files being * scoped. * @param Whitelist $whitelist List of classes that will not be scoped. * returning a boolean which if `true` means the class should be scoped * (i.e. is ignored) or scoped otherwise. * @param string[] $whitelistedFiles List of absolute paths of files to completely ignore */ private function __construct( ?string $path, ?string $prefix, array $filesWithContents, array $patchers, Whitelist $whitelist, array $whitelistedFiles ) { $this->path = $path; $this->prefix = $prefix; $this->filesWithContents = $filesWithContents; $this->patchers = $patchers; $this->whitelist = $whitelist; $this->whitelistedFiles = $whitelistedFiles; } public function withPaths(array $paths): self { $filesWithContents = self::retrieveFilesWithContents( chain( self::retrieveFilesFromPaths( array_unique($paths) ) ) ); return new self( $this->path, $this->prefix, array_merge($this->filesWithContents, $filesWithContents), $this->patchers, $this->whitelist, $this->whitelistedFiles ); } public function withPrefix(?string $prefix): self { $prefix = self::retrievePrefix([self::PREFIX_KEYWORD => $prefix]); return new self( $this->path, $prefix, $this->filesWithContents, $this->patchers, $this->whitelist, $this->whitelistedFiles ); } public function getPath(): ?string { return $this->path; } public function getPrefix(): ?string { return $this->prefix; } public function getFilesWithContents(): array { return $this->filesWithContents; } /** * @return callable[] */ public function getPatchers(): array { return $this->patchers; } public function getWhitelist(): Whitelist { return $this->whitelist; } /** * @return string[] */ public function getWhitelistedFiles(): array { return $this->whitelistedFiles; } private static function validateConfigKeys(array $config): void { array_map( [self::class, 'validateConfigKey'], array_keys($config) ); } private static function validateConfigKey(string $key): void { if (false === in_array($key, self::KEYWORDS, true)) { throw new InvalidArgumentException( sprintf( 'Invalid configuration key value "%s" found.', $key ) ); } } /** * If the prefix is set to null in the config file/argument then a random prefix is being used. However if set to * empty, the configuration will use a null prefix. * * TL:DR; setting the prefix is a big confusing because it is not properly split in "set prefix" & prefix strategy". */ private static function retrievePrefix(array $config): ?string { $prefix = $config[self::PREFIX_KEYWORD] ?? null; if (null === $prefix) { return null; } $prefix = trim($prefix); return '' === $prefix ? null : $prefix; } private static function retrievePatchers(array $config): array { if (false === array_key_exists(self::PATCHERS_KEYWORD, $config)) { return []; } $patchers = $config[self::PATCHERS_KEYWORD]; if (false === is_array($patchers)) { throw new InvalidArgumentException( sprintf( 'Expected patchers to be an array of callables, found "%s" instead.', gettype($patchers) ) ); } foreach ($patchers as $index => $patcher) { if (is_callable($patcher)) { continue; } throw new InvalidArgumentException( sprintf( 'Expected patchers to be an array of callables, the "%d" element is not.', $index ) ); } return $patchers; } private static function retrieveWhitelist(array $config): Whitelist { if (false === array_key_exists(self::WHITELIST_KEYWORD, $config)) { $whitelist = []; } else { $whitelist = $config[self::WHITELIST_KEYWORD]; if (false === is_array($whitelist)) { throw new InvalidArgumentException( sprintf( 'Expected whitelist to be an array of strings, found "%s" instead.', gettype($whitelist) ) ); } foreach ($whitelist as $index => $className) { if (is_string($className)) { continue; } throw new InvalidArgumentException( sprintf( 'Expected whitelist to be an array of string, the "%d" element is not.', $index ) ); } } if (false === array_key_exists(self::WHITELIST_GLOBAL_CONSTANTS_KEYWORD, $config)) { $whitelistGlobalConstants = true; } else { $whitelistGlobalConstants = $config[self::WHITELIST_GLOBAL_CONSTANTS_KEYWORD]; if (false === is_bool($whitelistGlobalConstants)) { throw new InvalidArgumentException( sprintf( 'Expected %s to be a boolean, found "%s" instead.', self::WHITELIST_GLOBAL_CONSTANTS_KEYWORD, gettype($whitelistGlobalConstants) ) ); } } if (false === array_key_exists(self::WHITELIST_GLOBAL_CLASSES_KEYWORD, $config)) { $whitelistGlobalClasses = true; } else { $whitelistGlobalClasses = $config[self::WHITELIST_GLOBAL_CLASSES_KEYWORD]; if (false === is_bool($whitelistGlobalClasses)) { throw new InvalidArgumentException( sprintf( 'Expected %s to be a boolean, found "%s" instead.', self::WHITELIST_GLOBAL_CLASSES_KEYWORD, gettype($whitelistGlobalClasses) ) ); } } if (false === array_key_exists(self::WHITELIST_GLOBAL_FUNCTIONS_KEYWORD, $config)) { $whitelistGlobalFunctions = true; } else { $whitelistGlobalFunctions = $config[self::WHITELIST_GLOBAL_FUNCTIONS_KEYWORD]; if (false === is_bool($whitelistGlobalFunctions)) { throw new InvalidArgumentException( sprintf( 'Expected %s to be a boolean, found "%s" instead.', self::WHITELIST_GLOBAL_FUNCTIONS_KEYWORD, gettype($whitelistGlobalFunctions) ) ); } } return Whitelist::create($whitelistGlobalConstants, $whitelistGlobalClasses, $whitelistGlobalFunctions, ...$whitelist); } /** * @return string[] Absolute paths */ private static function retrieveWhitelistedFiles(string $dirPath, array $config): array { if (false === array_key_exists(self::WHITELISTED_FILES_KEYWORD, $config)) { return []; } $whitelistedFiles = $config[self::WHITELISTED_FILES_KEYWORD]; if (false === is_array($whitelistedFiles)) { throw new InvalidArgumentException( sprintf( 'Expected whitelisted files to be an array of strings, found "%s" instead.', gettype($whitelistedFiles) ) ); } foreach ($whitelistedFiles as $index => $file) { if (false === is_string($file)) { throw new InvalidArgumentException( sprintf( 'Expected whitelisted files to be an array of string, the "%d" element is not.', $index ) ); } if (false === (new Filesystem())->isAbsolutePath($file)) { $file = $dirPath.DIRECTORY_SEPARATOR.$file; } $whitelistedFiles[$index] = realpath($file); } return array_filter($whitelistedFiles); } private static function retrieveFinders(array $config): array { if (false === array_key_exists(self::FINDER_KEYWORD, $config)) { return []; } $finders = $config[self::FINDER_KEYWORD]; if (false === is_array($finders)) { throw new InvalidArgumentException( sprintf( 'Expected finders to be an array of "%s", found "%s" instead.', Finder::class, gettype($finders) ) ); } foreach ($finders as $index => $finder) { if ($finder instanceof Finder) { continue; } throw new InvalidArgumentException( sprintf( 'Expected finders to be an array of "%s", the "%d" element is not.', Finder::class, $index ) ); } return $finders; } /** * @param string[] $paths */ private static function retrieveFilesFromPaths(array $paths): iterable { if ([] === $paths) { return []; } $pathsToSearch = []; $filesToAppend = []; foreach ($paths as $path) { if (false === file_exists($path)) { throw new RuntimeException( sprintf( 'Could not find the file "%s".', $path ) ); } if (is_dir($path)) { $pathsToSearch[] = $path; } else { $filesToAppend[] = $path; } } $finder = new Finder(); $finder->files() ->in($pathsToSearch) ->append($filesToAppend) ->filter(static function (SplFileInfo $fileInfo): ?bool { if ($fileInfo->isLink()) { return false; } return null; }) ->sortByName() ; return $finder; } /** * @return string[][] Array of tuple with the first argument being the file path and the second its contents */ private static function retrieveFilesWithContents(Iterator $files): array { return array_reduce( iterator_to_array($files, false), static function (array $files, SplFileInfo $fileInfo): array { $file = $fileInfo->getRealPath(); if (false === $file) { throw new RuntimeException( sprintf( 'Could not find the file "%s".', (string) $fileInfo ) ); } if (false === is_readable($file)) { throw new RuntimeException( sprintf( 'Could not read the file "%s".', $file ) ); } $files[$fileInfo->getRealPath()] = [$fileInfo->getRealPath(), file_get_contents($file)]; return $files; }, [] ); } }