ヤミRoot VoidGate
User / IP
:
216.73.216.143
Host / Server
:
146.88.233.70 / dev.loger.cm
System
:
Linux hybrid1120.fr.ns.planethoster.net 3.10.0-957.21.2.el7.x86_64 #1 SMP Wed Jun 5 14:26:44 UTC 2019 x86_64
Command
|
Upload
|
Create
Mass Deface
|
Jumping
|
Symlink
|
Reverse Shell
Ping
|
Port Scan
|
DNS Lookup
|
Whois
|
Header
|
cURL
:
/
home
/
logercm
/
dev.loger.cm
/
fixtures
/
assert
/
Viewing: flex.tar
src/Command/DumpEnvCommand.php 0000644 00000011406 15120140521 0012270 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Command; use Composer\Command\BaseCommand; use Composer\Config; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Dotenv\Dotenv; use Symfony\Flex\Options; class DumpEnvCommand extends BaseCommand { private $config; private $options; public function __construct(Config $config, Options $options) { $this->config = $config; $this->options = $options; parent::__construct(); } protected function configure() { $this->setName('symfony:dump-env') ->setAliases(['dump-env']) ->setDescription('Compiles .env files to .env.local.php.') ->setDefinition([ new InputArgument('env', InputArgument::OPTIONAL, 'The application environment to dump .env files for - e.g. "prod".'), ]) ->addOption('empty', null, InputOption::VALUE_NONE, 'Ignore the content of .env files') ; } protected function execute(InputInterface $input, OutputInterface $output): int { $runtime = $this->options->get('runtime') ?? []; $envKey = $runtime['env_var_name'] ?? 'APP_ENV'; if ($env = $input->getArgument('env') ?? $runtime['env'] ?? null) { $_SERVER[$envKey] = $env; } $path = $this->options->get('root-dir').'/'.($runtime['dotenv_path'] ?? '.env'); if (!$env || !$input->getOption('empty')) { $vars = $this->loadEnv($path, $env, $runtime); $env = $vars[$envKey]; } if ($input->getOption('empty')) { $vars = [$envKey => $env]; } $vars = var_export($vars, true); $vars = <<<EOF <?php // This file was generated by running "composer dump-env $env" return $vars; EOF; file_put_contents($path.'.local.php', $vars, \LOCK_EX); $this->getIO()->writeError('Successfully dumped .env files in <info>.env.local.php</>'); return 0; } private function loadEnv(string $path, ?string $env, array $runtime): array { if (!file_exists($autoloadFile = $this->config->get('vendor-dir').'/autoload.php')) { throw new \RuntimeException(sprintf('Please run "composer install" before running this command: "%s" not found.', $autoloadFile)); } require $autoloadFile; if (!class_exists(Dotenv::class)) { throw new \RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.'); } $envKey = $runtime['env_var_name'] ?? 'APP_ENV'; $globalsBackup = [$_SERVER, $_ENV]; unset($_SERVER[$envKey]); $_ENV = [$envKey => $env]; $_SERVER['SYMFONY_DOTENV_VARS'] = implode(',', array_keys($_SERVER)); putenv('SYMFONY_DOTENV_VARS='.$_SERVER['SYMFONY_DOTENV_VARS']); try { if (method_exists(Dotenv::class, 'usePutenv')) { $dotenv = new Dotenv(); } else { $dotenv = new Dotenv(false); } if (!$env && file_exists($p = "$path.local")) { $env = $_ENV[$envKey] = $dotenv->parse(file_get_contents($p), $p)[$envKey] ?? null; } if (!$env) { throw new \RuntimeException(sprintf('Please provide the name of the environment either by passing it as command line argument or by defining the "%s" variable in the ".env.local" file.', $envKey)); } $testEnvs = $runtime['test_envs'] ?? ['test']; if (method_exists($dotenv, 'loadEnv')) { $dotenv->loadEnv($path, $envKey, 'dev', $testEnvs); } else { // fallback code in case your Dotenv component is not 4.2 or higher (when loadEnv() was added) $dotenv->load(file_exists($path) || !file_exists($p = "$path.dist") ? $path : $p); if (!\in_array($env, $testEnvs, true) && file_exists($p = "$path.local")) { $dotenv->load($p); } if (file_exists($p = "$path.$env")) { $dotenv->load($p); } if (file_exists($p = "$path.$env.local")) { $dotenv->load($p); } } unset($_ENV['SYMFONY_DOTENV_VARS']); $env = $_ENV; } finally { list($_SERVER, $_ENV) = $globalsBackup; } return $env; } } src/Command/GenerateIdCommand.php 0000644 00000001731 15120140521 0012721 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Command; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; class GenerateIdCommand extends Command { public function __construct() { // No-op to support downgrading to v1.12.x parent::__construct(); } protected function configure() { $this->setName('symfony:generate-id'); } protected function execute(InputInterface $input, OutputInterface $output): int { $ui = new SymfonyStyle($input, $output); $ui->error('This command is a noop and should not be used anymore.'); return 1; } } src/Command/InstallRecipesCommand.php 0000644 00000016356 15120140521 0013644 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Command; use Composer\Command\BaseCommand; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\Util\ProcessExecutor; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Flex\Event\UpdateEvent; use Symfony\Flex\Flex; class InstallRecipesCommand extends BaseCommand { /** @var Flex */ private $flex; private $rootDir; private $dotenvPath; public function __construct(/* cannot be type-hinted */ $flex, string $rootDir, string $dotenvPath = '.env') { $this->flex = $flex; $this->rootDir = $rootDir; $this->dotenvPath = $dotenvPath; parent::__construct(); } protected function configure() { $this->setName('symfony:recipes:install') ->setAliases(['recipes:install', 'symfony:sync-recipes', 'sync-recipes', 'fix-recipes']) ->setDescription('Installs or reinstalls recipes for already installed packages.') ->addArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Recipes that should be installed.') ->addOption('force', null, InputOption::VALUE_NONE, 'Overwrite existing files when a new version of a recipe is available') ->addOption('reset', null, InputOption::VALUE_NONE, 'Reset all recipes back to their initial state (should be combined with --force)') ; } protected function execute(InputInterface $input, OutputInterface $output): int { $win = '\\' === \DIRECTORY_SEPARATOR; $force = (bool) $input->getOption('force'); if ($force && !@is_executable(strtok(exec($win ? 'where git' : 'command -v git'), \PHP_EOL))) { throw new RuntimeException('Cannot run "sync-recipes --force": git not found.'); } $symfonyLock = $this->flex->getLock(); $composer = $this->getComposer(); $locker = $composer->getLocker(); $lockData = $locker->getLockData(); $packages = []; $totalPackages = []; foreach ($lockData['packages'] as $pkg) { $totalPackages[] = $pkg['name']; if ($force || !$symfonyLock->has($pkg['name'])) { $packages[] = $pkg['name']; } } foreach ($lockData['packages-dev'] as $pkg) { $totalPackages[] = $pkg['name']; if ($force || !$symfonyLock->has($pkg['name'])) { $packages[] = $pkg['name']; } } $io = $this->getIO(); if (!$io->isVerbose()) { $io->writeError([ 'Run command with <info>-v</info> to see more details', '', ]); } if ($targetPackages = $input->getArgument('packages')) { if ($invalidPackages = array_diff($targetPackages, $totalPackages)) { $io->writeError(sprintf('<warning>Cannot update: some packages are not installed:</warning> %s', implode(', ', $invalidPackages))); return 1; } if ($packagesRequiringForce = array_diff($targetPackages, $packages)) { $io->writeError(sprintf('Recipe(s) already installed for: <info>%s</info>', implode(', ', $packagesRequiringForce))); $io->writeError('Re-run the command with <info>--force</info> to re-install the recipes.'); $io->writeError(''); } $packages = array_diff($targetPackages, $packagesRequiringForce); } if (!$packages) { $io->writeError('No recipes to install.'); return 0; } $composer = $this->getComposer(); $installedRepo = $composer->getRepositoryManager()->getLocalRepository(); $operations = []; foreach ($packages as $package) { if (null === $pkg = $installedRepo->findPackage($package, '*')) { $io->writeError(sprintf('<error>Package %s is not installed</>', $package)); return 1; } $operations[] = new InstallOperation($pkg); } $dotenvFile = $this->dotenvPath; $dotenvPath = $this->rootDir.'/'.$dotenvFile; if ($createEnvLocal = $force && file_exists($dotenvPath) && file_exists($dotenvPath.'.dist') && !file_exists($dotenvPath.'.local')) { rename($dotenvPath, $dotenvPath.'.local'); $pipes = []; proc_close(proc_open(sprintf('git mv %s %s > %s 2>&1 || %s %1$s %2$s', ProcessExecutor::escape($dotenvFile.'.dist'), ProcessExecutor::escape($dotenvFile), $win ? 'NUL' : '/dev/null', $win ? 'rename' : 'mv'), $pipes, $pipes, $this->rootDir)); if (file_exists($this->rootDir.'/phpunit.xml.dist')) { touch($dotenvPath.'.test'); } } $this->flex->update(new UpdateEvent($force, (bool) $input->getOption('reset')), $operations); if ($force) { $output = [ '', '<bg=blue;fg=white> </>', '<bg=blue;fg=white> Files have been reset to the latest version of the recipe. </>', '<bg=blue;fg=white> </>', '', ' * Use <comment>git diff</> to inspect the changes.', '', ' Not all of the changes will be relevant to your app: you now', ' need to selectively add or revert them using e.g. a combination', ' of <comment>git add -p</> and <comment>git checkout -p</>', '', ]; if ($createEnvLocal) { $output[] = ' Dotenv files have been renamed: .env -> .env.local and .env.dist -> .env'; $output[] = ' See https://symfony.com/doc/current/configuration/dot-env-changes.html'; $output[] = ''; } $output[] = ' * Use <comment>git checkout .</> to revert the changes.'; $output[] = ''; if ($createEnvLocal) { $root = '.' !== $this->rootDir ? $this->rootDir.'/' : ''; $output[] = ' To revert the changes made to .env files, run'; $output[] = sprintf(' <comment>git mv %s %s</> && <comment>%s %s %1$s</>', ProcessExecutor::escape($root.$dotenvFile), ProcessExecutor::escape($root.$dotenvFile.'.dist'), $win ? 'rename' : 'mv', ProcessExecutor::escape($root.$dotenvFile.'.local')); $output[] = ''; } $output[] = ' New (untracked) files can be inspected using <comment>git clean --dry-run</>'; $output[] = ' Add the new files you want to keep using <comment>git add</>'; $output[] = ' then delete the rest using <comment>git clean --force</>'; $output[] = ''; $io->write($output); } return 0; } } src/Command/RecipesCommand.php 0000644 00000025416 15120140521 0012312 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Command; use Composer\Command\BaseCommand; use Composer\Downloader\TransportException; use Composer\Package\Package; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Flex\GithubApi; use Symfony\Flex\InformationOperation; use Symfony\Flex\Lock; use Symfony\Flex\Recipe; /** * @author Maxime Hélias <maximehelias16@gmail.com> */ class RecipesCommand extends BaseCommand { /** @var \Symfony\Flex\Flex */ private $flex; private $symfonyLock; private $githubApi; public function __construct(/* cannot be type-hinted */ $flex, Lock $symfonyLock, $downloader) { $this->flex = $flex; $this->symfonyLock = $symfonyLock; $this->githubApi = new GithubApi($downloader); parent::__construct(); } protected function configure() { $this->setName('symfony:recipes') ->setAliases(['recipes']) ->setDescription('Shows information about all available recipes.') ->setDefinition([ new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect, if not provided all packages are.'), ]) ->addOption('outdated', 'o', InputOption::VALUE_NONE, 'Show only recipes that are outdated') ; } protected function execute(InputInterface $input, OutputInterface $output) { $installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository(); // Inspect one or all packages $package = $input->getArgument('package'); if (null !== $package) { $packages = [strtolower($package)]; } else { $locker = $this->getComposer()->getLocker(); $lockData = $locker->getLockData(); // Merge all packages installed $packages = array_column(array_merge($lockData['packages'], $lockData['packages-dev']), 'name'); $packages = array_unique(array_merge($packages, array_keys($this->symfonyLock->all()))); } $operations = []; foreach ($packages as $name) { $pkg = $installedRepo->findPackage($name, '*'); if (!$pkg && $this->symfonyLock->has($name)) { $pkgVersion = $this->symfonyLock->get($name)['version']; $pkg = new Package($name, $pkgVersion, $pkgVersion); } elseif (!$pkg) { $this->getIO()->writeError(sprintf('<error>Package %s is not installed</error>', $name)); continue; } $operations[] = new InformationOperation($pkg); } $recipes = $this->flex->fetchRecipes($operations, false); ksort($recipes); $nbRecipe = \count($recipes); if ($nbRecipe <= 0) { $this->getIO()->writeError('<error>No recipe found</error>'); return 1; } // Display the information about a specific recipe if (1 === $nbRecipe) { $this->displayPackageInformation(current($recipes)); return 0; } $outdated = $input->getOption('outdated'); $write = []; $hasOutdatedRecipes = false; /** @var Recipe $recipe */ foreach ($recipes as $name => $recipe) { $lockRef = $this->symfonyLock->get($name)['recipe']['ref'] ?? null; $additional = null; if (null === $lockRef && null !== $recipe->getRef()) { $additional = '<comment>(recipe not installed)</comment>'; } elseif ($recipe->getRef() !== $lockRef && !$recipe->isAuto()) { $additional = '<comment>(update available)</comment>'; } if ($outdated && null === $additional) { continue; } $hasOutdatedRecipes = true; $write[] = sprintf(' * %s %s', $name, $additional); } // Nothing to display if (!$hasOutdatedRecipes) { return 0; } $this->getIO()->write(array_merge([ '', '<bg=blue;fg=white> </>', sprintf('<bg=blue;fg=white> %s recipes. </>', $outdated ? ' Outdated' : 'Available'), '<bg=blue;fg=white> </>', '', ], $write, [ '', 'Run:', ' * <info>composer recipes vendor/package</info> to see details about a recipe.', ' * <info>composer recipes:update vendor/package</info> to update that recipe.', '', ])); if ($outdated) { return 1; } return 0; } private function displayPackageInformation(Recipe $recipe) { $io = $this->getIO(); $recipeLock = $this->symfonyLock->get($recipe->getName()); $lockRef = $recipeLock['recipe']['ref'] ?? null; $lockRepo = $recipeLock['recipe']['repo'] ?? null; $lockFiles = $recipeLock['files'] ?? null; $lockBranch = $recipeLock['recipe']['branch'] ?? null; $lockVersion = $recipeLock['recipe']['version'] ?? $recipeLock['version'] ?? null; if ('master' === $lockBranch && \in_array($lockRepo, ['github.com/symfony/recipes', 'github.com/symfony/recipes-contrib'])) { $lockBranch = 'main'; } $status = '<comment>up to date</comment>'; if ($recipe->isAuto()) { $status = '<comment>auto-generated recipe</comment>'; } elseif (null === $lockRef && null !== $recipe->getRef()) { $status = '<comment>recipe not installed</comment>'; } elseif ($recipe->getRef() !== $lockRef) { $status = '<comment>update available</comment>'; } $gitSha = null; $commitDate = null; if (null !== $lockRef && null !== $lockRepo) { try { $recipeCommitData = $this->githubApi->findRecipeCommitDataFromTreeRef( $recipe->getName(), $lockRepo, $lockBranch ?? '', $lockVersion, $lockRef ); $gitSha = $recipeCommitData ? $recipeCommitData['commit'] : null; $commitDate = $recipeCommitData ? $recipeCommitData['date'] : null; } catch (TransportException $exception) { $io->writeError('Error downloading exact git sha for installed recipe.'); } } $io->write('<info>name</info> : '.$recipe->getName()); $io->write('<info>version</info> : '.($lockVersion ?? 'n/a')); $io->write('<info>status</info> : '.$status); if (!$recipe->isAuto() && null !== $lockVersion) { $recipeUrl = sprintf( 'https://%s/tree/%s/%s/%s', $lockRepo, // if something fails, default to the branch as the closest "sha" $gitSha ?? $lockBranch, $recipe->getName(), $lockVersion ); $io->write('<info>installed recipe</info> : '.$recipeUrl); } if ($lockRef !== $recipe->getRef()) { $io->write('<info>latest recipe</info> : '.$recipe->getURL()); } if ($lockRef !== $recipe->getRef() && null !== $lockVersion) { $historyUrl = sprintf( 'https://%s/commits/%s/%s', $lockRepo, $lockBranch, $recipe->getName() ); // show commits since one second after the currently-installed recipe if (null !== $commitDate) { $historyUrl .= '?since='.(new \DateTime($commitDate))->modify('+1 seconds')->format('c\Z'); } $io->write('<info>recipe history</info> : '.$historyUrl); } if (null !== $lockFiles) { $io->write('<info>files</info> : '); $io->write(''); $tree = $this->generateFilesTree($lockFiles); $this->displayFilesTree($tree); } if ($lockRef !== $recipe->getRef()) { $io->write([ '', 'Update this recipe by running:', sprintf('<info>composer recipes:update %s</info>', $recipe->getName()), ]); } } private function generateFilesTree(array $files): array { $tree = []; foreach ($files as $file) { $path = explode('/', $file); $tree = array_merge_recursive($tree, $this->addNode($path)); } return $tree; } private function addNode(array $node): array { $current = array_shift($node); $subTree = []; if (null !== $current) { $subTree[$current] = $this->addNode($node); } return $subTree; } /** * Note : We do not display file modification information with Configurator like ComposerScripts, Container, DockerComposer, Dockerfile, Env, Gitignore and Makefile. */ private function displayFilesTree(array $tree) { end($tree); $endKey = key($tree); foreach ($tree as $dir => $files) { $treeBar = '├'; $total = \count($files); if (0 === $total || $endKey === $dir) { $treeBar = '└'; } $info = sprintf( '%s──%s', $treeBar, $dir ); $this->writeTreeLine($info); $treeBar = str_replace('└', ' ', $treeBar); $this->displayTree($files, $treeBar); } } private function displayTree(array $tree, $previousTreeBar = '├', $level = 1) { $previousTreeBar = str_replace('├', '│', $previousTreeBar); $treeBar = $previousTreeBar.' ├'; $i = 0; $total = \count($tree); foreach ($tree as $dir => $files) { ++$i; if ($i === $total) { $treeBar = $previousTreeBar.' └'; } $info = sprintf( '%s──%s', $treeBar, $dir ); $this->writeTreeLine($info); $treeBar = str_replace('└', ' ', $treeBar); $this->displayTree($files, $treeBar, $level + 1); } } private function writeTreeLine($line) { $io = $this->getIO(); if (!$io->isDecorated()) { $line = str_replace(['└', '├', '──', '│'], ['`-', '|-', '-', '|'], $line); } $io->write($line); } } src/Command/UnpackCommand.php 0000644 00000010375 15120140521 0012137 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Command; use Composer\Command\BaseCommand; use Composer\Factory; use Composer\Installer; use Composer\Package\Version\VersionParser; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Flex\PackageResolver; use Symfony\Flex\Unpack\Operation; use Symfony\Flex\Unpacker; /** * @deprecated since Flex 1.4 */ class UnpackCommand extends BaseCommand { private $resolver; public function __construct(PackageResolver $resolver) { $this->resolver = $resolver; parent::__construct(); } protected function configure() { $this->setName('symfony:unpack') ->setAliases(['unpack']) ->setDescription('[DEPRECATED] Unpacks a Symfony pack.') ->setDefinition([ new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Installed packages to unpack.'), new InputOption('sort-packages', null, InputOption::VALUE_NONE, 'Sorts packages'), ]) ; } protected function execute(InputInterface $input, OutputInterface $output) { $composer = $this->getComposer(); $packages = $this->resolver->resolve($input->getArgument('packages'), true); $io = $this->getIO(); $lockData = $composer->getLocker()->getLockData(); $installedRepo = $composer->getRepositoryManager()->getLocalRepository(); $versionParser = new VersionParser(); $dryRun = $input->hasOption('dry-run') && $input->getOption('dry-run'); $io->writeError('<warning>Command "symfony:unpack" is deprecated, Symfony packs are always unpacked now.</>'); $op = new Operation(true, $input->getOption('sort-packages') || $composer->getConfig()->get('sort-packages')); foreach ($versionParser->parseNameVersionPairs($packages) as $package) { if (null === $pkg = $installedRepo->findPackage($package['name'], '*')) { $io->writeError(sprintf('<error>Package %s is not installed</>', $package['name'])); return 1; } $dev = false; foreach ($lockData['packages-dev'] as $p) { if ($package['name'] === $p['name']) { $dev = true; break; } } $op->addPackage($pkg->getName(), $pkg->getVersion(), $dev); } $unpacker = new Unpacker($composer, $this->resolver, $dryRun); $result = $unpacker->unpack($op); // remove the packages themselves if (!$result->getUnpacked()) { $io->writeError('<info>Nothing to unpack</>'); return 0; } $io->writeError('<info>Unpacking Symfony packs</>'); foreach ($result->getUnpacked() as $pkg) { $io->writeError(sprintf(' - Unpacked <info>%s</>', $pkg->getName())); } $unpacker->updateLock($result, $io); if ($input->hasOption('no-install') && $input->getOption('no-install')) { return 0; } $composer = Factory::create($io, null, true); $installer = Installer::create($io, $composer); $installer ->setDryRun($dryRun) ->setDevMode(true) ->setDumpAutoloader(false) ->setIgnorePlatformRequirements(true) ->setUpdate(true) ; if (method_exists($installer, 'setUpdateAllowList')) { $installer->setUpdateAllowList(['php']); } else { $installer->setUpdateWhiteList(['php']); } if (method_exists($composer->getEventDispatcher(), 'setRunScripts')) { $composer->getEventDispatcher()->setRunScripts(false); } else { $installer->setRunScripts(false); } if (method_exists($installer, 'setSkipSuggest')) { $installer->setSkipSuggest(true); } return $installer->run(); } } src/Command/UpdateRecipesCommand.php 0000644 00000036173 15120140521 0013457 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Command; use Composer\Command\BaseCommand; use Composer\IO\IOInterface; use Composer\Package\Package; use Composer\Package\PackageInterface; use Composer\Util\ProcessExecutor; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Flex\Configurator; use Symfony\Flex\Downloader; use Symfony\Flex\Flex; use Symfony\Flex\GithubApi; use Symfony\Flex\InformationOperation; use Symfony\Flex\Lock; use Symfony\Flex\Recipe; use Symfony\Flex\Update\RecipePatcher; use Symfony\Flex\Update\RecipeUpdate; class UpdateRecipesCommand extends BaseCommand { /** @var Flex */ private $flex; private $downloader; private $configurator; private $rootDir; private $githubApi; private $processExecutor; public function __construct(/* cannot be type-hinted */ $flex, Downloader $downloader, $httpDownloader, Configurator $configurator, string $rootDir) { $this->flex = $flex; $this->downloader = $downloader; $this->configurator = $configurator; $this->rootDir = $rootDir; $this->githubApi = new GithubApi($httpDownloader); parent::__construct(); } protected function configure() { $this->setName('symfony:recipes:update') ->setAliases(['recipes:update']) ->setDescription('Updates an already-installed recipe to the latest version.') ->addArgument('package', InputArgument::OPTIONAL, 'Recipe that should be updated.') ; } protected function execute(InputInterface $input, OutputInterface $output): int { $win = '\\' === \DIRECTORY_SEPARATOR; $runtimeExceptionClass = class_exists(RuntimeException::class) ? RuntimeException::class : \RuntimeException::class; if (!@is_executable(strtok(exec($win ? 'where git' : 'command -v git'), \PHP_EOL))) { throw new $runtimeExceptionClass('Cannot run "recipes:update": git not found.'); } $io = $this->getIO(); if (!$this->isIndexClean($io)) { $io->write([ ' Cannot run <comment>recipes:update</comment>: Your git index contains uncommitted changes.', ' Please commit or stash them and try again!', ]); return 1; } $packageName = $input->getArgument('package'); $symfonyLock = $this->flex->getLock(); if (!$packageName) { $packageName = $this->askForPackage($io, $symfonyLock); if (null === $packageName) { $io->writeError('All packages appear to be up-to-date!'); return 0; } } if (!$symfonyLock->has($packageName)) { $io->writeError([ 'Package not found inside symfony.lock. It looks like it\'s not installed?', sprintf('Try running <info>composer recipes:install %s --force -v</info> to re-install the recipe.', $packageName), ]); return 1; } $packageLockData = $symfonyLock->get($packageName); if (!isset($packageLockData['recipe'])) { $io->writeError([ 'It doesn\'t look like this package had a recipe when it was originally installed.', 'To install the latest version of the recipe, if there is one, run:', sprintf(' <info>composer recipes:install %s --force -v</info>', $packageName), ]); return 1; } $recipeRef = $packageLockData['recipe']['ref'] ?? null; $recipeVersion = $packageLockData['recipe']['version'] ?? null; if (!$recipeRef || !$recipeVersion) { $io->writeError([ 'The version of the installed recipe was not saved into symfony.lock.', 'This is possible if it was installed by an old version of Symfony Flex.', 'Update the recipe by re-installing the latest version with:', sprintf(' <info>composer recipes:install %s --force -v</info>', $packageName), ]); return 1; } $installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository(); $package = $installedRepo->findPackage($packageName, '*') ?? new Package($packageName, $packageLockData['version'], $packageLockData['version']); $originalRecipe = $this->getRecipe($package, $recipeRef, $recipeVersion); if (null === $originalRecipe) { $io->writeError([ 'The original recipe version you have installed could not be found, it may be too old.', 'Update the recipe by re-installing the latest version with:', sprintf(' <info>composer recipes:install %s --force -v</info>', $packageName), ]); return 1; } $newRecipe = $this->getRecipe($package); if ($newRecipe->getRef() === $originalRecipe->getRef()) { $io->write(sprintf('This recipe for <info>%s</info> is already at the latest version.', $packageName)); return 0; } $io->write([ sprintf(' Updating recipe for <info>%s</info>...', $packageName), '', ]); $recipeUpdate = new RecipeUpdate($originalRecipe, $newRecipe, $symfonyLock, $this->rootDir); $this->configurator->populateUpdate($recipeUpdate); $originalComposerJsonHash = $this->flex->getComposerJsonHash(); $patcher = new RecipePatcher($this->rootDir, $io); try { $patch = $patcher->generatePatch($recipeUpdate->getOriginalFiles(), $recipeUpdate->getNewFiles()); $hasConflicts = !$patcher->applyPatch($patch); } catch (\Throwable $throwable) { $io->writeError([ '<bg=red;fg=white>There was an error applying the recipe update patch</>', $throwable->getMessage(), '', 'Update the recipe by re-installing the latest version with:', sprintf(' <info>composer recipes:install %s --force -v</info>', $packageName), ]); return 1; } $symfonyLock->add($packageName, $newRecipe->getLock()); $this->flex->finish($this->rootDir, $originalComposerJsonHash); // stage symfony.lock, as all patched files with already be staged $cmdOutput = ''; $this->getProcessExecutor()->execute('git add symfony.lock', $cmdOutput, $this->rootDir); $io->write([ ' <bg=blue;fg=white> </>', ' <bg=blue;fg=white> Yes! Recipe updated! </>', ' <bg=blue;fg=white> </>', '', ]); if ($hasConflicts) { $io->write([ ' The recipe was updated but with <bg=red;fg=white>one or more conflicts</>.', ' Run <comment>git status</comment> to see them.', ' After resolving, commit your changes like normal.', ]); } else { if (!$patch->getPatch()) { // no changes were required $io->write([ ' No files were changed as a result of the update.', ]); } else { $io->write([ ' Run <comment>git status</comment> or <comment>git diff --cached</comment> to see the changes.', ' When you\'re ready, commit these changes like normal.', ]); } } if (0 !== \count($recipeUpdate->getCopyFromPackagePaths())) { $io->write([ '', ' <bg=red;fg=white>NOTE:</>', ' This recipe copies the following paths from the bundle into your app:', ]); foreach ($recipeUpdate->getCopyFromPackagePaths() as $source => $target) { $io->write(sprintf(' * %s => %s', $source, $target)); } $io->write([ '', ' The recipe updater has no way of knowing if these files have changed since you originally installed the recipe.', ' And so, no updates were made to these paths.', ]); } if (0 !== \count($patch->getRemovedPatches())) { if (1 === \count($patch->getRemovedPatches())) { $notes = [ sprintf(' The file <comment>%s</comment> was not updated because it doesn\'t exist in your app.', array_keys($patch->getRemovedPatches())[0]), ]; } else { $notes = [' The following files were not updated because they don\'t exist in your app:']; foreach ($patch->getRemovedPatches() as $filename => $contents) { $notes[] = sprintf(' * <comment>%s</comment>', $filename); } } $io->write([ '', ' <bg=red;fg=white>NOTE:</>', ]); $io->write($notes); $io->write(''); if ($io->askConfirmation(' Would you like to save the "diff" to a file so you can review it? (Y/n) ')) { $patchFilename = str_replace('/', '.', $packageName).'.updates-for-deleted-files.patch'; file_put_contents($this->rootDir.'/'.$patchFilename, implode("\n", $patch->getRemovedPatches())); $io->write([ '', sprintf(' Saved diff to <info>%s</info>', $patchFilename), ]); } } if ($patch->getPatch()) { $io->write(''); $io->write(' Calculating CHANGELOG...', false); $changelog = $this->generateChangelog($originalRecipe); $io->write("\r", false); // clear current line if ($changelog) { $io->write($changelog); } else { $io->write('No CHANGELOG could be calculated.'); } } return 0; } private function getRecipe(PackageInterface $package, string $recipeRef = null, string $recipeVersion = null): ?Recipe { $operation = new InformationOperation($package); if (null !== $recipeRef) { $operation->setSpecificRecipeVersion($recipeRef, $recipeVersion); } $recipes = $this->downloader->getRecipes([$operation]); if (0 === \count($recipes['manifests'] ?? [])) { return null; } return new Recipe( $package, $package->getName(), $operation->getOperationType(), $recipes['manifests'][$package->getName()], $recipes['locks'][$package->getName()] ?? [] ); } private function generateChangelog(Recipe $originalRecipe): ?array { $recipeData = $originalRecipe->getLock()['recipe'] ?? null; if (null === $recipeData) { return null; } if (!isset($recipeData['ref']) || !isset($recipeData['repo']) || !isset($recipeData['branch']) || !isset($recipeData['version'])) { return null; } $currentRecipeVersionData = $this->githubApi->findRecipeCommitDataFromTreeRef( $originalRecipe->getName(), $recipeData['repo'], $recipeData['branch'], $recipeData['version'], $recipeData['ref'] ); if (!$currentRecipeVersionData) { return null; } $recipeVersions = $this->githubApi->getVersionsOfRecipe( $recipeData['repo'], $recipeData['branch'], $originalRecipe->getName() ); if (!$recipeVersions) { return null; } $newerRecipeVersions = array_filter($recipeVersions, function ($version) use ($recipeData) { return version_compare($version, $recipeData['version'], '>'); }); $newCommits = $currentRecipeVersionData['new_commits']; foreach ($newerRecipeVersions as $newerRecipeVersion) { $newCommits = array_merge( $newCommits, $this->githubApi->getCommitDataForPath($recipeData['repo'], $originalRecipe->getName().'/'.$newerRecipeVersion, $recipeData['branch']) ); } $newCommits = array_unique($newCommits); asort($newCommits); $pullRequests = []; foreach ($newCommits as $commit => $date) { $pr = $this->githubApi->getPullRequestForCommit($commit, $recipeData['repo']); if ($pr) { $pullRequests[$pr['number']] = $pr; } } $lines = []; // borrowed from symfony/console's OutputFormatterStyle $handlesHrefGracefully = 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR') && (!getenv('KONSOLE_VERSION') || (int) getenv('KONSOLE_VERSION') > 201100); foreach ($pullRequests as $number => $data) { $url = $data['url']; if ($handlesHrefGracefully) { $url = "\033]8;;$url\033\\$number\033]8;;\033\\"; } $lines[] = sprintf(' * %s (PR %s)', $data['title'], $url); } return $lines; } private function askForPackage(IOInterface $io, Lock $symfonyLock): ?string { $installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository(); $operations = []; foreach ($symfonyLock->all() as $name => $lock) { if (isset($lock['recipe']['ref'])) { $package = $installedRepo->findPackage($name, '*') ?? new Package($name, $lock['version'], $lock['version']); $operations[] = new InformationOperation($package); } } $recipes = $this->flex->fetchRecipes($operations, false); ksort($recipes); $outdatedRecipes = []; foreach ($recipes as $name => $recipe) { $lockRef = $symfonyLock->get($name)['recipe']['ref'] ?? null; if (null !== $lockRef && $recipe->getRef() !== $lockRef && !$recipe->isAuto()) { $outdatedRecipes[] = $name; } } if (0 === \count($outdatedRecipes)) { return null; } $question = 'Which outdated recipe would you like to update? (default: <info>0</info>)'; $choice = $io->select( $question, $outdatedRecipes, 0 ); return $outdatedRecipes[$choice]; } private function isIndexClean(IOInterface $io): bool { $output = ''; $this->getProcessExecutor()->execute('git status --porcelain --untracked-files=no', $output, $this->rootDir); if ('' !== trim($output)) { return false; } return true; } private function getProcessExecutor(): ProcessExecutor { if (null === $this->processExecutor) { $this->processExecutor = new ProcessExecutor($this->getIO()); } return $this->processExecutor; } } src/Configurator/AbstractConfigurator.php 0000644 00000007672 15120140521 0014637 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Configurator; use Composer\Composer; use Composer\IO\IOInterface; use Symfony\Flex\Lock; use Symfony\Flex\Options; use Symfony\Flex\Path; use Symfony\Flex\Recipe; use Symfony\Flex\Update\RecipeUpdate; /** * @author Fabien Potencier <fabien@symfony.com> */ abstract class AbstractConfigurator { protected $composer; protected $io; protected $options; protected $path; public function __construct(Composer $composer, IOInterface $io, Options $options) { $this->composer = $composer; $this->io = $io; $this->options = $options; $this->path = new Path($options->get('root-dir')); } abstract public function configure(Recipe $recipe, $config, Lock $lock, array $options = []); abstract public function unconfigure(Recipe $recipe, $config, Lock $lock); abstract public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void; protected function write($messages, $verbosity = IOInterface::VERBOSE) { if (!\is_array($messages)) { $messages = [$messages]; } foreach ($messages as $i => $message) { $messages[$i] = ' '.$message; } $this->io->writeError($messages, true, $verbosity); } protected function isFileMarked(Recipe $recipe, string $file): bool { return is_file($file) && false !== strpos(file_get_contents($file), sprintf('###> %s ###', $recipe->getName())); } protected function markData(Recipe $recipe, string $data): string { return "\n".sprintf('###> %s ###%s%s%s###< %s ###%s', $recipe->getName(), "\n", rtrim($data, "\r\n"), "\n", $recipe->getName(), "\n"); } protected function isFileXmlMarked(Recipe $recipe, string $file): bool { return is_file($file) && false !== strpos(file_get_contents($file), sprintf('###+ %s ###', $recipe->getName())); } protected function markXmlData(Recipe $recipe, string $data): string { return "\n".sprintf(' <!-- ###+ %s ### -->%s%s%s <!-- ###- %s ### -->%s', $recipe->getName(), "\n", rtrim($data, "\r\n"), "\n", $recipe->getName(), "\n"); } /** * @return bool True if section was found and replaced */ protected function updateData(string $file, string $data): bool { if (!file_exists($file)) { return false; } $contents = file_get_contents($file); $newContents = $this->updateDataString($contents, $data); if (null === $newContents) { return false; } file_put_contents($file, $newContents); return true; } /** * @return string|null returns the updated content if the section was found, null if not found */ protected function updateDataString(string $contents, string $data): ?string { $pieces = explode("\n", trim($data)); $startMark = trim(reset($pieces)); $endMark = trim(end($pieces)); if (false === strpos($contents, $startMark) || false === strpos($contents, $endMark)) { return null; } $pattern = '/'.preg_quote($startMark, '/').'.*?'.preg_quote($endMark, '/').'/s'; return preg_replace($pattern, trim($data), $contents); } protected function extractSection(Recipe $recipe, string $contents): ?string { $section = $this->markData($recipe, '----'); $pieces = explode("\n", trim($section)); $startMark = trim(reset($pieces)); $endMark = trim(end($pieces)); $pattern = '/'.preg_quote($startMark, '/').'.*?'.preg_quote($endMark, '/').'/s'; $matches = []; preg_match($pattern, $contents, $matches); return $matches[0] ?? null; } } src/Configurator/AddLinesConfigurator.php 0000644 00000020437 15120140521 0014551 0 ustar 00 <?php namespace Symfony\Flex\Configurator; use Composer\IO\IOInterface; use Symfony\Flex\Lock; use Symfony\Flex\Recipe; use Symfony\Flex\Update\RecipeUpdate; /** * @author Kevin Bond <kevinbond@gmail.com> * @author Ryan Weaver <ryan@symfonycasts.com> */ class AddLinesConfigurator extends AbstractConfigurator { private const POSITION_TOP = 'top'; private const POSITION_BOTTOM = 'bottom'; private const POSITION_AFTER_TARGET = 'after_target'; private const VALID_POSITIONS = [ self::POSITION_TOP, self::POSITION_BOTTOM, self::POSITION_AFTER_TARGET, ]; public function configure(Recipe $recipe, $config, Lock $lock, array $options = []): void { foreach ($config as $patch) { if (!isset($patch['file'])) { $this->write(sprintf('The "file" key is required for the "add-lines" configurator for recipe "%s". Skipping', $recipe->getName())); continue; } if (isset($patch['requires']) && !$this->isPackageInstalled($patch['requires'])) { continue; } if (!isset($patch['content'])) { $this->write(sprintf('The "content" key is required for the "add-lines" configurator for recipe "%s". Skipping', $recipe->getName())); continue; } $content = $patch['content']; $file = $this->path->concatenate([$this->options->get('root-dir'), $patch['file']]); $warnIfMissing = isset($patch['warn_if_missing']) && $patch['warn_if_missing']; if (!is_file($file)) { $this->write([ sprintf('Could not add lines to file <info>%s</info> as it does not exist. Missing lines:', $patch['file']), '<comment>"""</comment>', $content, '<comment>"""</comment>', '', ], $warnIfMissing ? IOInterface::NORMAL : IOInterface::VERBOSE); continue; } $this->write(sprintf('Patching file "%s"', $patch['file'])); if (!isset($patch['position'])) { $this->write(sprintf('The "position" key is required for the "add-lines" configurator for recipe "%s". Skipping', $recipe->getName())); continue; } $position = $patch['position']; if (!\in_array($position, self::VALID_POSITIONS, true)) { $this->write(sprintf('The "position" key must be one of "%s" for the "add-lines" configurator for recipe "%s". Skipping', implode('", "', self::VALID_POSITIONS), $recipe->getName())); continue; } if (self::POSITION_AFTER_TARGET === $position && !isset($patch['target'])) { $this->write(sprintf('The "target" key is required when "position" is "%s" for the "add-lines" configurator for recipe "%s". Skipping', self::POSITION_AFTER_TARGET, $recipe->getName())); continue; } $target = isset($patch['target']) ? $patch['target'] : null; $this->patchFile($file, $content, $position, $target, $warnIfMissing); } } public function unconfigure(Recipe $recipe, $config, Lock $lock): void { foreach ($config as $patch) { if (!isset($patch['file'])) { $this->write(sprintf('The "file" key is required for the "add-lines" configurator for recipe "%s". Skipping', $recipe->getName())); continue; } // Ignore "requires": the target packages may have just become uninstalled. // Checking for a "content" match is enough. $file = $this->path->concatenate([$this->options->get('root-dir'), $patch['file']]); if (!is_file($file)) { continue; } if (!isset($patch['content'])) { $this->write(sprintf('The "content" key is required for the "add-lines" configurator for recipe "%s". Skipping', $recipe->getName())); continue; } $value = $patch['content']; $this->unPatchFile($file, $value); } } public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void { $originalConfig = array_filter($originalConfig, function ($item) { return !isset($item['requires']) || $this->isPackageInstalled($item['requires']); }); $newConfig = array_filter($newConfig, function ($item) { return !isset($item['requires']) || $this->isPackageInstalled($item['requires']); }); $filterDuplicates = function (array $sourceConfig, array $comparisonConfig) { $filtered = []; foreach ($sourceConfig as $sourceItem) { $found = false; foreach ($comparisonConfig as $comparisonItem) { if ($sourceItem['file'] === $comparisonItem['file'] && $sourceItem['content'] === $comparisonItem['content']) { $found = true; break; } } if (!$found) { $filtered[] = $sourceItem; } } return $filtered; }; // remove any config where the file+value is the same before & after $filteredOriginalConfig = $filterDuplicates($originalConfig, $newConfig); $filteredNewConfig = $filterDuplicates($newConfig, $originalConfig); $this->unconfigure($recipeUpdate->getOriginalRecipe(), $filteredOriginalConfig, $recipeUpdate->getLock()); $this->configure($recipeUpdate->getNewRecipe(), $filteredNewConfig, $recipeUpdate->getLock()); } private function patchFile(string $file, string $value, string $position, ?string $target, bool $warnIfMissing) { $fileContents = file_get_contents($file); if (false !== strpos($fileContents, $value)) { return; // already includes value, skip } switch ($position) { case self::POSITION_BOTTOM: $fileContents .= "\n".$value; break; case self::POSITION_TOP: $fileContents = $value."\n".$fileContents; break; case self::POSITION_AFTER_TARGET: $lines = explode("\n", $fileContents); $targetFound = false; foreach ($lines as $key => $line) { if (false !== strpos($line, $target)) { array_splice($lines, $key + 1, 0, $value); $targetFound = true; break; } } $fileContents = implode("\n", $lines); if (!$targetFound) { $this->write([ sprintf('Could not add lines after "%s" as no such string was found in "%s". Missing lines:', $target, $file), '<comment>"""</comment>', $value, '<comment>"""</comment>', '', ], $warnIfMissing ? IOInterface::NORMAL : IOInterface::VERBOSE); } break; } file_put_contents($file, $fileContents); } private function unPatchFile(string $file, $value) { $fileContents = file_get_contents($file); if (false === strpos($fileContents, $value)) { return; // value already gone! } if (false !== strpos($fileContents, "\n".$value)) { $value = "\n".$value; } elseif (false !== strpos($fileContents, $value."\n")) { $value = $value."\n"; } $position = strpos($fileContents, $value); $fileContents = substr_replace($fileContents, '', $position, \strlen($value)); file_put_contents($file, $fileContents); } private function isPackageInstalled($packages): bool { if (\is_string($packages)) { $packages = [$packages]; } $installedRepo = $this->composer->getRepositoryManager()->getLocalRepository(); foreach ($packages as $packageName) { if (null === $installedRepo->findPackage($packageName, '*')) { return false; } } return true; } } src/Configurator/BundlesConfigurator.php 0000644 00000010707 15120140521 0014461 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Configurator; use Symfony\Flex\Lock; use Symfony\Flex\Recipe; use Symfony\Flex\Update\RecipeUpdate; /** * @author Fabien Potencier <fabien@symfony.com> */ class BundlesConfigurator extends AbstractConfigurator { public function configure(Recipe $recipe, $bundles, Lock $lock, array $options = []) { $this->write('Enabling the package as a Symfony bundle'); $registered = $this->configureBundles($bundles); $this->dump($this->getConfFile(), $registered); } public function unconfigure(Recipe $recipe, $bundles, Lock $lock) { $this->write('Disabling the Symfony bundle'); $file = $this->getConfFile(); if (!file_exists($file)) { return; } $registered = $this->load($file); foreach (array_keys($this->prepareBundles($bundles)) as $class) { unset($registered[$class]); } $this->dump($file, $registered); } public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void { $originalBundles = $this->configureBundles($originalConfig, true); $recipeUpdate->setOriginalFile( $this->getLocalConfFile(), $this->buildContents($originalBundles) ); $newBundles = $this->configureBundles($newConfig, true); $recipeUpdate->setNewFile( $this->getLocalConfFile(), $this->buildContents($newBundles) ); } private function configureBundles(array $bundles, bool $resetEnvironments = false): array { $file = $this->getConfFile(); $registered = $this->load($file); $classes = $this->prepareBundles($bundles); if (isset($classes[$fwb = 'Symfony\Bundle\FrameworkBundle\FrameworkBundle'])) { foreach ($classes[$fwb] as $env) { $registered[$fwb][$env] = true; } unset($classes[$fwb]); } foreach ($classes as $class => $envs) { // do not override existing configured envs for a bundle if (!isset($registered[$class]) || $resetEnvironments) { if ($resetEnvironments) { // used during calculating an "upgrade" // here, we want to "undo" the bundle's configuration entirely // then re-add it fresh, in case some environments have been // removed in an updated version of the recipe $registered[$class] = []; } foreach ($envs as $env) { $registered[$class][$env] = true; } } } return $registered; } private function prepareBundles(array $bundles): array { foreach ($bundles as $class => $envs) { $bundles[ltrim($class, '\\')] = $envs; } return $bundles; } private function load(string $file): array { $bundles = file_exists($file) ? (require $file) : []; if (!\is_array($bundles)) { $bundles = []; } return $bundles; } private function dump(string $file, array $bundles) { $contents = $this->buildContents($bundles); if (!is_dir(\dirname($file))) { mkdir(\dirname($file), 0777, true); } file_put_contents($file, $contents); if (\function_exists('opcache_invalidate')) { opcache_invalidate($file); } } private function buildContents(array $bundles): string { $contents = "<?php\n\nreturn [\n"; foreach ($bundles as $class => $envs) { $contents .= " $class::class => ["; foreach ($envs as $env => $value) { $booleanValue = var_export($value, true); $contents .= "'$env' => $booleanValue, "; } $contents = substr($contents, 0, -2)."],\n"; } $contents .= "];\n"; return $contents; } private function getConfFile(): string { return $this->options->get('root-dir').'/'.$this->getLocalConfFile(); } private function getLocalConfFile(): string { return $this->options->expandTargetDir('%CONFIG_DIR%/bundles.php'); } } src/Configurator/ComposerScriptsConfigurator.php 0000644 00000004532 15120140521 0016223 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Configurator; use Composer\Factory; use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; use Symfony\Flex\Lock; use Symfony\Flex\Recipe; use Symfony\Flex\Update\RecipeUpdate; /** * @author Fabien Potencier <fabien@symfony.com> */ class ComposerScriptsConfigurator extends AbstractConfigurator { public function configure(Recipe $recipe, $scripts, Lock $lock, array $options = []) { $json = new JsonFile(Factory::getComposerFile()); file_put_contents($json->getPath(), $this->configureScripts($scripts, $json)); } public function unconfigure(Recipe $recipe, $scripts, Lock $lock) { $json = new JsonFile(Factory::getComposerFile()); $jsonContents = $json->read(); $autoScripts = $jsonContents['scripts']['auto-scripts'] ?? []; foreach (array_keys($scripts) as $cmd) { unset($autoScripts[$cmd]); } $manipulator = new JsonManipulator(file_get_contents($json->getPath())); $manipulator->addSubNode('scripts', 'auto-scripts', $autoScripts); file_put_contents($json->getPath(), $manipulator->getContents()); } public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void { $json = new JsonFile(Factory::getComposerFile()); $jsonPath = ltrim(str_replace($recipeUpdate->getRootDir(), '', $json->getPath()), '/\\'); $recipeUpdate->setOriginalFile( $jsonPath, $this->configureScripts($originalConfig, $json) ); $recipeUpdate->setNewFile( $jsonPath, $this->configureScripts($newConfig, $json) ); } private function configureScripts(array $scripts, JsonFile $json): string { $jsonContents = $json->read(); $autoScripts = $jsonContents['scripts']['auto-scripts'] ?? []; $autoScripts = array_merge($autoScripts, $scripts); $manipulator = new JsonManipulator(file_get_contents($json->getPath())); $manipulator->addSubNode('scripts', 'auto-scripts', $autoScripts); return $manipulator->getContents(); } } src/Configurator/ContainerConfigurator.php 0000644 00000012322 15120140521 0015002 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Configurator; use Symfony\Flex\Lock; use Symfony\Flex\Recipe; use Symfony\Flex\Update\RecipeUpdate; /** * @author Fabien Potencier <fabien@symfony.com> */ class ContainerConfigurator extends AbstractConfigurator { public function configure(Recipe $recipe, $parameters, Lock $lock, array $options = []) { $this->write('Setting parameters'); $contents = $this->configureParameters($parameters); if (null !== $contents) { file_put_contents($this->options->get('root-dir').'/'.$this->getServicesPath(), $contents); } } public function unconfigure(Recipe $recipe, $parameters, Lock $lock) { $this->write('Unsetting parameters'); $target = $this->options->get('root-dir').'/'.$this->getServicesPath(); $lines = $this->removeParametersFromLines(file($target), $parameters); file_put_contents($target, implode('', $lines)); } public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void { $recipeUpdate->setOriginalFile( $this->getServicesPath(), $this->configureParameters($originalConfig, true) ); // for the new file, we need to update any values *and* remove any removed values $removedParameters = []; foreach ($originalConfig as $name => $value) { if (!isset($newConfig[$name])) { $removedParameters[$name] = $value; } } $updatedFile = $this->configureParameters($newConfig, true); $lines = $this->removeParametersFromLines(explode("\n", $updatedFile), $removedParameters); $recipeUpdate->setNewFile( $this->getServicesPath(), implode("\n", $lines) ); } private function configureParameters(array $parameters, bool $update = false): string { $target = $this->options->get('root-dir').'/'.$this->getServicesPath(); $endAt = 0; $isParameters = false; $lines = []; foreach (file($target) as $i => $line) { $lines[] = $line; if (!$isParameters && !preg_match('/^parameters:/', $line)) { continue; } if (!$isParameters) { $isParameters = true; continue; } if (!preg_match('/^\s+.*/', $line) && '' !== trim($line)) { $endAt = $i - 1; $isParameters = false; continue; } foreach ($parameters as $key => $value) { $matches = []; if (preg_match(sprintf('/^\s+%s\:/', preg_quote($key, '/')), $line, $matches)) { if ($update) { $lines[$i] = substr($line, 0, \strlen($matches[0])).' '.str_replace("'", "''", $value)."\n"; } unset($parameters[$key]); } } } if ($parameters) { $parametersLines = []; if (!$endAt) { $parametersLines[] = "parameters:\n"; } foreach ($parameters as $key => $value) { if (\is_array($value)) { $parametersLines[] = sprintf(" %s:\n%s", $key, $this->dumpYaml(2, $value)); continue; } $parametersLines[] = sprintf(" %s: '%s'%s", $key, str_replace("'", "''", $value), "\n"); } if (!$endAt) { $parametersLines[] = "\n"; } array_splice($lines, $endAt, 0, $parametersLines); } return implode('', $lines); } private function removeParametersFromLines(array $sourceLines, array $parameters): array { $lines = []; foreach ($sourceLines as $line) { if ($this->removeParameters(1, $parameters, $line)) { continue; } $lines[] = $line; } return $lines; } private function removeParameters($level, $params, $line) { foreach ($params as $key => $value) { if (\is_array($value) && $this->removeParameters($level + 1, $value, $line)) { return true; } if (preg_match(sprintf('/^(\s{%d}|\t{%d})+%s\:/', 4 * $level, $level, preg_quote($key, '/')), $line)) { return true; } } return false; } private function dumpYaml($level, $array): string { $line = ''; foreach ($array as $key => $value) { $line .= str_repeat(' ', $level); if (!\is_array($value)) { $line .= sprintf("%s: '%s'\n", $key, str_replace("'", "''", $value)); continue; } $line .= sprintf("%s:\n", $key).$this->dumpYaml($level + 1, $value); } return $line; } private function getServicesPath(): string { return $this->options->expandTargetDir('%CONFIG_DIR%/services.yaml'); } } src/Configurator/CopyFromPackageConfigurator.php 0000644 00000014403 15120140521 0016074 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Configurator; use Symfony\Flex\Lock; use Symfony\Flex\Recipe; use Symfony\Flex\Update\RecipeUpdate; /** * @author Fabien Potencier <fabien@symfony.com> */ class CopyFromPackageConfigurator extends AbstractConfigurator { public function configure(Recipe $recipe, $config, Lock $lock, array $options = []) { $this->write('Copying files from package'); $packageDir = $this->composer->getInstallationManager()->getInstallPath($recipe->getPackage()); $options = array_merge($this->options->toArray(), $options); $files = $this->getFilesToCopy($config, $packageDir); foreach ($files as $source => $target) { $this->copyFile($source, $target, $options); } } public function unconfigure(Recipe $recipe, $config, Lock $lock) { $this->write('Removing files from package'); $packageDir = $this->composer->getInstallationManager()->getInstallPath($recipe->getPackage()); $this->removeFiles($config, $packageDir, $this->options->get('root-dir')); } public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void { $packageDir = $this->composer->getInstallationManager()->getInstallPath($recipeUpdate->getNewRecipe()->getPackage()); foreach ($originalConfig as $source => $target) { if (isset($newConfig[$source])) { // path is in both, we cannot update $recipeUpdate->addCopyFromPackagePath( $packageDir.'/'.$source, $this->options->expandTargetDir($target) ); unset($newConfig[$source]); } // if any paths were removed from the recipe, we'll keep them } // any remaining files are new, and we can copy them foreach ($this->getFilesToCopy($newConfig, $packageDir) as $source => $target) { if (!file_exists($source)) { throw new \LogicException(sprintf('File "%s" does not exist!', $source)); } $recipeUpdate->setNewFile($target, file_get_contents($source)); } } private function getFilesToCopy(array $manifest, string $from): array { $files = []; foreach ($manifest as $source => $target) { $target = $this->options->expandTargetDir($target); if ('/' === substr($source, -1)) { $files = array_merge($files, $this->getFilesForDir($this->path->concatenate([$from, $source]), $this->path->concatenate([$target]))); continue; } $files[$this->path->concatenate([$from, $source])] = $target; } return $files; } private function removeFiles(array $manifest, string $from, string $to) { foreach ($manifest as $source => $target) { $target = $this->options->expandTargetDir($target); if ('/' === substr($source, -1)) { $this->removeFilesFromDir($this->path->concatenate([$from, $source]), $this->path->concatenate([$to, $target])); } else { $targetPath = $this->path->concatenate([$to, $target]); if (file_exists($targetPath)) { @unlink($targetPath); $this->write(sprintf(' Removed <fg=green>"%s"</>', $this->path->relativize($targetPath))); } } } } private function getFilesForDir(string $source, string $target): array { $iterator = $this->createSourceIterator($source, \RecursiveIteratorIterator::SELF_FIRST); $files = []; foreach ($iterator as $item) { $targetPath = $this->path->concatenate([$target, $iterator->getSubPathName()]); $files[(string) $item] = $targetPath; } return $files; } /** * @param string $source The absolute path to the source file * @param string $target The relative (to root dir) path to the target */ public function copyFile(string $source, string $target, array $options) { $target = $this->options->get('root-dir').'/'.$target; if (is_dir($source)) { // directory will be created when a file is copied to it return; } $overwrite = $options['force'] ?? false; if (!$this->options->shouldWriteFile($target, $overwrite)) { return; } if (!file_exists($source)) { throw new \LogicException(sprintf('File "%s" does not exist!', $source)); } if (!file_exists(\dirname($target))) { mkdir(\dirname($target), 0777, true); $this->write(sprintf(' Created <fg=green>"%s"</>', $this->path->relativize(\dirname($target)))); } file_put_contents($target, $this->options->expandTargetDir(file_get_contents($source))); @chmod($target, fileperms($target) | (fileperms($source) & 0111)); $this->write(sprintf(' Created <fg=green>"%s"</>', $this->path->relativize($target))); } private function removeFilesFromDir(string $source, string $target) { if (!is_dir($source)) { return; } $iterator = $this->createSourceIterator($source, \RecursiveIteratorIterator::CHILD_FIRST); foreach ($iterator as $item) { $targetPath = $this->path->concatenate([$target, $iterator->getSubPathName()]); if ($item->isDir()) { // that removes the dir only if it is empty @rmdir($targetPath); $this->write(sprintf(' Removed directory <fg=green>"%s"</>', $this->path->relativize($targetPath))); } else { @unlink($targetPath); $this->write(sprintf(' Removed <fg=green>"%s"</>', $this->path->relativize($targetPath))); } } } private function createSourceIterator(string $source, int $mode): \RecursiveIteratorIterator { return new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS), $mode); } } src/Configurator/CopyFromRecipeConfigurator.php 0000644 00000013406 15120140521 0015752 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Configurator; use Symfony\Flex\Lock; use Symfony\Flex\Recipe; use Symfony\Flex\Update\RecipeUpdate; /** * @author Fabien Potencier <fabien@symfony.com> */ class CopyFromRecipeConfigurator extends AbstractConfigurator { public function configure(Recipe $recipe, $config, Lock $lock, array $options = []) { $this->write('Copying files from recipe'); $options = array_merge($this->options->toArray(), $options); $lock->add($recipe->getName(), ['files' => $this->copyFiles($config, $recipe->getFiles(), $options)]); } public function unconfigure(Recipe $recipe, $config, Lock $lock) { $this->write('Removing files from recipe'); $this->removeFiles($config, $this->getRemovableFilesFromRecipeAndLock($recipe, $lock), $this->options->get('root-dir')); } public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void { foreach ($recipeUpdate->getOriginalRecipe()->getFiles() as $filename => $data) { $recipeUpdate->setOriginalFile($filename, $data['contents']); } $files = []; foreach ($recipeUpdate->getNewRecipe()->getFiles() as $filename => $data) { $recipeUpdate->setNewFile($filename, $data['contents']); $files[] = $this->getLocalFilePath($recipeUpdate->getRootDir(), $filename); } $recipeUpdate->getLock()->add($recipeUpdate->getPackageName(), ['files' => $files]); } private function getRemovableFilesFromRecipeAndLock(Recipe $recipe, Lock $lock): array { $lockedFiles = array_unique( array_reduce( array_column($lock->all(), 'files'), function (array $carry, array $package) { return array_merge($carry, $package); }, [] ) ); $removableFiles = $recipe->getFiles(); $lockedFiles = array_map('realpath', $lockedFiles); // Compare file paths by their real path to abstract OS differences foreach (array_keys($removableFiles) as $file) { if (\in_array(realpath($file), $lockedFiles)) { unset($removableFiles[$file]); } } return $removableFiles; } private function copyFiles(array $manifest, array $files, array $options): array { $copiedFiles = []; $to = $options['root-dir'] ?? '.'; foreach ($manifest as $source => $target) { $target = $this->options->expandTargetDir($target); if ('/' === substr($source, -1)) { $copiedFiles = array_merge( $copiedFiles, $this->copyDir($source, $this->path->concatenate([$to, $target]), $files, $options) ); } else { $copiedFiles[] = $this->copyFile($this->path->concatenate([$to, $target]), $files[$source]['contents'], $files[$source]['executable'], $options); } } return $copiedFiles; } private function copyDir(string $source, string $target, array $files, array $options): array { $copiedFiles = []; foreach ($files as $file => $data) { if (0 === strpos($file, $source)) { $file = $this->path->concatenate([$target, substr($file, \strlen($source))]); $copiedFiles[] = $this->copyFile($file, $data['contents'], $data['executable'], $options); } } return $copiedFiles; } private function copyFile(string $to, string $contents, bool $executable, array $options): string { $overwrite = $options['force'] ?? false; $basePath = $options['root-dir'] ?? '.'; $copiedFile = $this->getLocalFilePath($basePath, $to); if (!$this->options->shouldWriteFile($to, $overwrite)) { return $copiedFile; } if (!is_dir(\dirname($to))) { mkdir(\dirname($to), 0777, true); } file_put_contents($to, $this->options->expandTargetDir($contents)); if ($executable) { @chmod($to, fileperms($to) | 0111); } $this->write(sprintf(' Created <fg=green>"%s"</>', $this->path->relativize($to))); return $copiedFile; } private function removeFiles(array $manifest, array $files, string $to) { foreach ($manifest as $source => $target) { $target = $this->options->expandTargetDir($target); if ('.git' === $target) { // never remove the main Git directory, even if it was created by a recipe continue; } if ('/' === substr($source, -1)) { foreach (array_keys($files) as $file) { if (0 === strpos($file, $source)) { $this->removeFile($this->path->concatenate([$to, $target, substr($file, \strlen($source))])); } } } else { $this->removeFile($this->path->concatenate([$to, $target])); } } } private function removeFile(string $to) { if (!file_exists($to)) { return; } @unlink($to); $this->write(sprintf(' Removed <fg=green>"%s"</>', $this->path->relativize($to))); if (0 === \count(glob(\dirname($to).'/*', \GLOB_NOSORT))) { @rmdir(\dirname($to)); } } private function getLocalFilePath(string $basePath, $destination): string { return str_replace($basePath.\DIRECTORY_SEPARATOR, '', $destination); } } src/Configurator/DockerComposeConfigurator.php 0000644 00000033205 15120140521 0015620 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Configurator; use Composer\Composer; use Composer\Factory; use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; use Symfony\Component\Filesystem\Filesystem; use Symfony\Flex\Lock; use Symfony\Flex\Options; use Symfony\Flex\Recipe; use Symfony\Flex\Update\RecipeUpdate; /** * Adds services and volumes to docker-compose.yml file. * * @author Kévin Dunglas <dunglas@gmail.com> */ class DockerComposeConfigurator extends AbstractConfigurator { private $filesystem; public static $configureDockerRecipes = null; public function __construct(Composer $composer, IOInterface $io, Options $options) { parent::__construct($composer, $io, $options); $this->filesystem = new Filesystem(); } public function configure(Recipe $recipe, $config, Lock $lock, array $options = []) { if (!self::shouldConfigureDockerRecipe($this->composer, $this->io, $recipe)) { return; } $this->configureDockerCompose($recipe, $config, $options['force'] ?? false); $this->write('Docker Compose definitions have been modified. Please run "docker compose up --build" again to apply the changes.'); } public function unconfigure(Recipe $recipe, $config, Lock $lock) { $rootDir = $this->options->get('root-dir'); foreach ($this->normalizeConfig($config) as $file => $extra) { if (null === $dockerComposeFile = $this->findDockerComposeFile($rootDir, $file)) { continue; } $name = $recipe->getName(); // Remove recipe and add break line $contents = preg_replace(sprintf('{%s+###> %s ###.*?###< %s ###%s+}s', "\n", $name, $name, "\n"), \PHP_EOL.\PHP_EOL, file_get_contents($dockerComposeFile), -1, $count); if (!$count) { return; } foreach ($extra as $key => $value) { if (0 === preg_match(sprintf('{^%s:[ \t\r\n]*([ \t]+\w|#)}m', $key), $contents, $matches)) { $contents = preg_replace(sprintf('{\n?^%s:[ \t\r\n]*}sm', $key), '', $contents, -1, $count); } } $this->write(sprintf('Removing Docker Compose entries from "%s"', $dockerComposeFile)); file_put_contents($dockerComposeFile, ltrim($contents, "\n")); } $this->write('Docker Compose definitions have been modified. Please run "docker compose up" again to apply the changes.'); } public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void { if (!self::shouldConfigureDockerRecipe($this->composer, $this->io, $recipeUpdate->getNewRecipe())) { return; } $recipeUpdate->addOriginalFiles( $this->getContentsAfterApplyingRecipe($recipeUpdate->getRootDir(), $recipeUpdate->getOriginalRecipe(), $originalConfig) ); $recipeUpdate->addNewFiles( $this->getContentsAfterApplyingRecipe($recipeUpdate->getRootDir(), $recipeUpdate->getNewRecipe(), $newConfig) ); } public static function shouldConfigureDockerRecipe(Composer $composer, IOInterface $io, Recipe $recipe): bool { if (null !== self::$configureDockerRecipes) { return self::$configureDockerRecipes; } if (null !== $dockerPreference = $composer->getPackage()->getExtra()['symfony']['docker'] ?? null) { self::$configureDockerRecipes = $dockerPreference; return self::$configureDockerRecipes; } if ('install' !== $recipe->getJob()) { // default to not configuring return false; } if (!isset($_SERVER['SYMFONY_DOCKER'])) { $answer = self::askDockerSupport($io, $recipe); } elseif (filter_var($_SERVER['SYMFONY_DOCKER'], \FILTER_VALIDATE_BOOLEAN)) { $answer = 'p'; } else { $answer = 'x'; } if ('n' === $answer) { self::$configureDockerRecipes = false; return self::$configureDockerRecipes; } if ('y' === $answer) { self::$configureDockerRecipes = true; return self::$configureDockerRecipes; } // yes or no permanently self::$configureDockerRecipes = 'p' === $answer; $json = new JsonFile(Factory::getComposerFile()); $manipulator = new JsonManipulator(file_get_contents($json->getPath())); $manipulator->addSubNode('extra', 'symfony.docker', self::$configureDockerRecipes); file_put_contents($json->getPath(), $manipulator->getContents()); return self::$configureDockerRecipes; } /** * Normalizes the config and return the name of the main Docker Compose file if applicable. */ private function normalizeConfig(array $config): array { foreach ($config as $val) { // Support for the short syntax recipe syntax that modifies docker-compose.yml only return isset($val[0]) ? ['docker-compose.yml' => $config] : $config; } return $config; } /** * Finds the Docker Compose file according to these rules: https://docs.docker.com/compose/reference/envvars/#compose_file. */ private function findDockerComposeFile(string $rootDir, string $file): ?string { if (isset($_SERVER['COMPOSE_FILE'])) { $separator = $_SERVER['COMPOSE_PATH_SEPARATOR'] ?? ('\\' === \DIRECTORY_SEPARATOR ? ';' : ':'); $files = explode($separator, $_SERVER['COMPOSE_FILE']); foreach ($files as $f) { if ($file !== basename($f)) { continue; } if (!$this->filesystem->isAbsolutePath($f)) { $f = realpath(sprintf('%s/%s', $rootDir, $f)); } if ($this->filesystem->exists($f)) { return $f; } } } // COMPOSE_FILE not set, or doesn't contain the file we're looking for $dir = $rootDir; do { // Test with the ".yaml" extension if the file doesn't end up with ".yml". if ( $this->filesystem->exists($dockerComposeFile = sprintf('%s/%s', $dir, $file)) || $this->filesystem->exists($dockerComposeFile = substr($dockerComposeFile, 0, -2).'aml') ) { return $dockerComposeFile; } $previousDir = $dir; $dir = \dirname($dir); } while ($dir !== $previousDir); return null; } private function parse($level, $indent, $services): string { $line = ''; foreach ($services as $key => $value) { $line .= str_repeat(' ', $indent * $level); if (!\is_array($value)) { if (\is_string($key)) { $line .= sprintf('%s:', $key); } $line .= sprintf("%s\n", $value); continue; } $line .= sprintf("%s:\n", $key).$this->parse($level + 1, $indent, $value); } return $line; } private function configureDockerCompose(Recipe $recipe, array $config, bool $update): void { $rootDir = $this->options->get('root-dir'); foreach ($this->normalizeConfig($config) as $file => $extra) { $dockerComposeFile = $this->findDockerComposeFile($rootDir, $file); if (null === $dockerComposeFile) { $dockerComposeFile = $rootDir.'/'.$file; file_put_contents($dockerComposeFile, "version: '3'\n"); $this->write(sprintf(' Created <fg=green>"%s"</>', $file)); } if (!$update && $this->isFileMarked($recipe, $dockerComposeFile)) { continue; } $this->write(sprintf('Adding Docker Compose definitions to "%s"', $dockerComposeFile)); $offset = 2; $node = null; $endAt = []; $startAt = []; $lines = []; $nodesLines = []; foreach (file($dockerComposeFile) as $i => $line) { $lines[] = $line; $ltrimedLine = ltrim($line, ' '); if (null !== $node) { $nodesLines[$node][$i] = $line; } // Skip blank lines and comments if (('' !== $ltrimedLine && 0 === strpos($ltrimedLine, '#')) || '' === trim($line)) { continue; } // Extract Docker Compose keys (usually "services" and "volumes") if (!preg_match('/^[\'"]?([a-zA-Z0-9]+)[\'"]?:\s*$/', $line, $matches)) { // Detect indentation to use $offestLine = \strlen($line) - \strlen($ltrimedLine); if ($offset > $offestLine && 0 !== $offestLine) { $offset = $offestLine; } continue; } // Keep end in memory (check break line on previous line) $endAt[$node] = !$i || '' !== trim($lines[$i - 1]) ? $i : $i - 1; $node = $matches[1]; if (!isset($nodesLines[$node])) { $nodesLines[$node] = []; } if (!isset($startAt[$node])) { // the section contents starts at the next line $startAt[$node] = $i + 1; } } $endAt[$node] = \count($lines) + 1; foreach ($extra as $key => $value) { if (isset($endAt[$key])) { $data = $this->markData($recipe, $this->parse(1, $offset, $value)); $updatedContents = $this->updateDataString(implode('', $nodesLines[$key]), $data); if (null === $updatedContents) { // not an update: just add to section array_splice($lines, $endAt[$key], 0, $data); continue; } $originalEndAt = $endAt[$key]; $length = $endAt[$key] - $startAt[$key]; array_splice($lines, $startAt[$key], $length, ltrim($updatedContents, "\n")); // reset any start/end positions after this to the new positions foreach ($startAt as $sectionKey => $at) { if ($at > $originalEndAt) { $startAt[$sectionKey] = $at - $length - 1; } } foreach ($endAt as $sectionKey => $at) { if ($at > $originalEndAt) { $endAt[$sectionKey] = $at - $length; } } continue; } $lines[] = sprintf("\n%s:", $key); $lines[] = $this->markData($recipe, $this->parse(1, $offset, $value)); } file_put_contents($dockerComposeFile, implode('', $lines)); } } private function getContentsAfterApplyingRecipe(string $rootDir, Recipe $recipe, array $config): array { if (0 === \count($config)) { return []; } $files = array_filter(array_map(function ($file) use ($rootDir) { return $this->findDockerComposeFile($rootDir, $file); }, array_keys($config))); $originalContents = []; foreach ($files as $file) { $originalContents[$file] = file_exists($file) ? file_get_contents($file) : null; } $this->configureDockerCompose( $recipe, $config, true ); $updatedContents = []; foreach ($files as $file) { $localPath = ltrim(str_replace($rootDir, '', $file), '/\\'); $updatedContents[$localPath] = file_exists($file) ? file_get_contents($file) : null; } foreach ($originalContents as $file => $contents) { if (null === $contents) { if (file_exists($file)) { unlink($file); } } else { file_put_contents($file, $contents); } } return $updatedContents; } private static function askDockerSupport(IOInterface $io, Recipe $recipe): string { $warning = $io->isInteractive() ? 'WARNING' : 'IGNORING'; $io->writeError(sprintf(' - <warning> %s </> %s', $warning, $recipe->getFormattedOrigin())); $question = ' The recipe for this package contains some Docker configuration. This may create/update <comment>docker-compose.yml</comment> or update <comment>Dockerfile</comment> (if it exists). Do you want to include Docker configuration from recipes? [<comment>y</>] Yes [<comment>n</>] No [<comment>p</>] Yes permanently, never ask again for this project [<comment>x</>] No permanently, never ask again for this project (defaults to <comment>y</>): '; return $io->askAndValidate( $question, function ($value) { if (null === $value) { return 'y'; } $value = strtolower($value[0]); if (!\in_array($value, ['y', 'n', 'p', 'x'], true)) { throw new \InvalidArgumentException('Invalid choice.'); } return $value; }, null, 'y' ); } } src/Configurator/DockerfileConfigurator.php 0000644 00000007306 15120140521 0015135 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Configurator; use Symfony\Flex\Lock; use Symfony\Flex\Recipe; use Symfony\Flex\Update\RecipeUpdate; /** * Adds commands to a Dockerfile. * * @author Kévin Dunglas <dunglas@gmail.com> */ class DockerfileConfigurator extends AbstractConfigurator { public function configure(Recipe $recipe, $config, Lock $lock, array $options = []) { if (!DockerComposeConfigurator::shouldConfigureDockerRecipe($this->composer, $this->io, $recipe)) { return; } $this->configureDockerfile($recipe, $config, $options['force'] ?? false); } public function unconfigure(Recipe $recipe, $config, Lock $lock) { if (!file_exists($dockerfile = $this->options->get('root-dir').'/Dockerfile')) { return; } $name = $recipe->getName(); $contents = preg_replace(sprintf('{%s+###> %s ###.*?###< %s ###%s+}s', "\n", $name, $name, "\n"), "\n", file_get_contents($dockerfile), -1, $count); if (!$count) { return; } $this->write('Removing Dockerfile entries'); file_put_contents($dockerfile, ltrim($contents, "\n")); } public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void { if (!DockerComposeConfigurator::shouldConfigureDockerRecipe($this->composer, $this->io, $recipeUpdate->getNewRecipe())) { return; } $recipeUpdate->setOriginalFile( 'Dockerfile', $this->getContentsAfterApplyingRecipe($recipeUpdate->getOriginalRecipe(), $originalConfig) ); $recipeUpdate->setNewFile( 'Dockerfile', $this->getContentsAfterApplyingRecipe($recipeUpdate->getNewRecipe(), $newConfig) ); } private function configureDockerfile(Recipe $recipe, array $config, bool $update, bool $writeOutput = true): void { $dockerfile = $this->options->get('root-dir').'/Dockerfile'; if (!file_exists($dockerfile) || (!$update && $this->isFileMarked($recipe, $dockerfile))) { return; } if ($writeOutput) { $this->write('Adding Dockerfile entries'); } $data = ltrim($this->markData($recipe, implode("\n", $config)), "\n"); if ($this->updateData($dockerfile, $data)) { // done! Existing spot updated return; } $lines = []; foreach (file($dockerfile) as $line) { $lines[] = $line; if (!preg_match('/^###> recipes ###$/', $line)) { continue; } $lines[] = $data; } file_put_contents($dockerfile, implode('', $lines)); } private function getContentsAfterApplyingRecipe(Recipe $recipe, array $config): ?string { if (0 === \count($config)) { return null; } $dockerfile = $this->options->get('root-dir').'/Dockerfile'; $originalContents = file_exists($dockerfile) ? file_get_contents($dockerfile) : null; $this->configureDockerfile( $recipe, $config, true, false ); $updatedContents = file_exists($dockerfile) ? file_get_contents($dockerfile) : null; if (null === $originalContents) { if (file_exists($dockerfile)) { unlink($dockerfile); } } else { file_put_contents($dockerfile, $originalContents); } return $updatedContents; } } src/Configurator/EnvConfigurator.php 0000644 00000022514 15120140521 0013614 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Configurator; use Symfony\Flex\Lock; use Symfony\Flex\Recipe; use Symfony\Flex\Update\RecipeUpdate; /** * @author Fabien Potencier <fabien@symfony.com> */ class EnvConfigurator extends AbstractConfigurator { public function configure(Recipe $recipe, $vars, Lock $lock, array $options = []) { $this->write('Adding environment variable defaults'); $this->configureEnvDist($recipe, $vars, $options['force'] ?? false); if (!file_exists($this->options->get('root-dir').'/'.($this->options->get('runtime')['dotenv_path'] ?? '.env').'.test')) { $this->configurePhpUnit($recipe, $vars, $options['force'] ?? false); } } public function unconfigure(Recipe $recipe, $vars, Lock $lock) { $this->unconfigureEnvFiles($recipe, $vars); $this->unconfigurePhpUnit($recipe, $vars); } public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void { $recipeUpdate->addOriginalFiles( $this->getContentsAfterApplyingRecipe($recipeUpdate->getRootDir(), $recipeUpdate->getOriginalRecipe(), $originalConfig) ); $recipeUpdate->addNewFiles( $this->getContentsAfterApplyingRecipe($recipeUpdate->getRootDir(), $recipeUpdate->getNewRecipe(), $newConfig) ); } private function configureEnvDist(Recipe $recipe, $vars, bool $update) { $dotenvPath = $this->options->get('runtime')['dotenv_path'] ?? '.env'; foreach ([$dotenvPath.'.dist', $dotenvPath] as $file) { $env = $this->options->get('root-dir').'/'.$file; if (!is_file($env)) { continue; } if (!$update && $this->isFileMarked($recipe, $env)) { continue; } $data = ''; foreach ($vars as $key => $value) { $existingValue = $update ? $this->findExistingValue($key, $env, $recipe) : null; $value = $this->evaluateValue($value, $existingValue); if ('#' === $key[0] && is_numeric(substr($key, 1))) { if ('' === $value) { $data .= "#\n"; } else { $data .= '# '.$value."\n"; } continue; } $value = $this->options->expandTargetDir($value); if (false !== strpbrk($value, " \t\n&!\"")) { $value = '"'.str_replace(['\\', '"', "\t", "\n"], ['\\\\', '\\"', '\t', '\n'], $value).'"'; } $data .= "$key=$value\n"; } $data = $this->markData($recipe, $data); if (!$this->updateData($env, $data)) { file_put_contents($env, $data, \FILE_APPEND); } } } private function configurePhpUnit(Recipe $recipe, $vars, bool $update) { foreach (['phpunit.xml.dist', 'phpunit.xml'] as $file) { $phpunit = $this->options->get('root-dir').'/'.$file; if (!is_file($phpunit)) { continue; } if (!$update && $this->isFileXmlMarked($recipe, $phpunit)) { continue; } $data = ''; foreach ($vars as $key => $value) { $value = $this->evaluateValue($value); if ('#' === $key[0]) { if (is_numeric(substr($key, 1))) { $doc = new \DOMDocument(); $data .= ' '.$doc->saveXML($doc->createComment(' '.$value.' '))."\n"; } else { $value = $this->options->expandTargetDir($value); $doc = new \DOMDocument(); $fragment = $doc->createElement('env'); $fragment->setAttribute('name', substr($key, 1)); $fragment->setAttribute('value', $value); $data .= ' '.str_replace(['<', '/>'], ['<!-- ', ' -->'], $doc->saveXML($fragment))."\n"; } } else { $value = $this->options->expandTargetDir($value); $doc = new \DOMDocument(); $fragment = $doc->createElement('env'); $fragment->setAttribute('name', $key); $fragment->setAttribute('value', $value); $data .= ' '.$doc->saveXML($fragment)."\n"; } } $data = $this->markXmlData($recipe, $data); if (!$this->updateData($phpunit, $data)) { file_put_contents($phpunit, preg_replace('{^(\s+</php>)}m', $data.'$1', file_get_contents($phpunit))); } } } private function unconfigureEnvFiles(Recipe $recipe, $vars) { $dotenvPath = $this->options->get('runtime')['dotenv_path'] ?? '.env'; foreach ([$dotenvPath, $dotenvPath.'.dist'] as $file) { $env = $this->options->get('root-dir').'/'.$file; if (!file_exists($env)) { continue; } $contents = preg_replace(sprintf('{%s*###> %s ###.*###< %s ###%s+}s', "\n", $recipe->getName(), $recipe->getName(), "\n"), "\n", file_get_contents($env), -1, $count); if (!$count) { continue; } $this->write(sprintf('Removing environment variables from %s', $file)); file_put_contents($env, $contents); } } private function unconfigurePhpUnit(Recipe $recipe, $vars) { foreach (['phpunit.xml.dist', 'phpunit.xml'] as $file) { $phpunit = $this->options->get('root-dir').'/'.$file; if (!is_file($phpunit)) { continue; } $contents = preg_replace(sprintf('{%s*\s+<!-- ###\+ %s ### -->.*<!-- ###- %s ### -->%s+}s', "\n", $recipe->getName(), $recipe->getName(), "\n"), "\n", file_get_contents($phpunit), -1, $count); if (!$count) { continue; } $this->write(sprintf('Removing environment variables from %s', $file)); file_put_contents($phpunit, $contents); } } /** * Evaluates expressions like %generate(secret)%. * * If $originalValue is passed, and the value contains an expression. * the $originalValue is used. */ private function evaluateValue($value, string $originalValue = null) { if ('%generate(secret)%' === $value) { if (null !== $originalValue) { return $originalValue; } return $this->generateRandomBytes(); } if (preg_match('~^%generate\(secret,\s*([0-9]+)\)%$~', $value, $matches)) { if (null !== $originalValue) { return $originalValue; } return $this->generateRandomBytes($matches[1]); } return $value; } private function generateRandomBytes($length = 16) { return bin2hex(random_bytes($length)); } private function getContentsAfterApplyingRecipe(string $rootDir, Recipe $recipe, array $vars): array { $dotenvPath = $this->options->get('runtime')['dotenv_path'] ?? '.env'; $files = [$dotenvPath, $dotenvPath.'.dist', 'phpunit.xml.dist', 'phpunit.xml']; if (0 === \count($vars)) { return array_fill_keys($files, null); } $originalContents = []; foreach ($files as $file) { $originalContents[$file] = file_exists($rootDir.'/'.$file) ? file_get_contents($rootDir.'/'.$file) : null; } $this->configureEnvDist( $recipe, $vars, true ); if (!file_exists($rootDir.'/'.$dotenvPath.'.test')) { $this->configurePhpUnit( $recipe, $vars, true ); } $updatedContents = []; foreach ($files as $file) { $updatedContents[$file] = file_exists($rootDir.'/'.$file) ? file_get_contents($rootDir.'/'.$file) : null; } foreach ($originalContents as $file => $contents) { if (null === $contents) { if (file_exists($rootDir.'/'.$file)) { unlink($rootDir.'/'.$file); } } else { file_put_contents($rootDir.'/'.$file, $contents); } } return $updatedContents; } /** * Attempts to find the existing value of an environment variable. */ private function findExistingValue(string $var, string $filename, Recipe $recipe): ?string { if (!file_exists($filename)) { return null; } $contents = file_get_contents($filename); $section = $this->extractSection($recipe, $contents); if (!$section) { return null; } $lines = explode("\n", $section); foreach ($lines as $line) { if (0 !== strpos($line, sprintf('%s=', $var))) { continue; } return trim(substr($line, \strlen($var) + 1)); } return null; } } src/Configurator/GitignoreConfigurator.php 0000644 00000006111 15120140521 0015006 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Configurator; use Symfony\Flex\Lock; use Symfony\Flex\Recipe; use Symfony\Flex\Update\RecipeUpdate; /** * @author Fabien Potencier <fabien@symfony.com> */ class GitignoreConfigurator extends AbstractConfigurator { public function configure(Recipe $recipe, $vars, Lock $lock, array $options = []) { $this->write('Adding entries to .gitignore'); $this->configureGitignore($recipe, $vars, $options['force'] ?? false); } public function unconfigure(Recipe $recipe, $vars, Lock $lock) { $file = $this->options->get('root-dir').'/.gitignore'; if (!file_exists($file)) { return; } $contents = preg_replace(sprintf('{%s*###> %s ###.*###< %s ###%s+}s', "\n", $recipe->getName(), $recipe->getName(), "\n"), "\n", file_get_contents($file), -1, $count); if (!$count) { return; } $this->write('Removing entries in .gitignore'); file_put_contents($file, ltrim($contents, "\r\n")); } public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void { $recipeUpdate->setOriginalFile( '.gitignore', $this->getContentsAfterApplyingRecipe($recipeUpdate->getRootDir(), $recipeUpdate->getOriginalRecipe(), $originalConfig) ); $recipeUpdate->setNewFile( '.gitignore', $this->getContentsAfterApplyingRecipe($recipeUpdate->getRootDir(), $recipeUpdate->getNewRecipe(), $newConfig) ); } private function configureGitignore(Recipe $recipe, array $vars, bool $update) { $gitignore = $this->options->get('root-dir').'/.gitignore'; if (!$update && $this->isFileMarked($recipe, $gitignore)) { return; } $data = ''; foreach ($vars as $value) { $value = $this->options->expandTargetDir($value); $data .= "$value\n"; } $data = "\n".ltrim($this->markData($recipe, $data), "\r\n"); if (!$this->updateData($gitignore, $data)) { file_put_contents($gitignore, $data, \FILE_APPEND); } } private function getContentsAfterApplyingRecipe(string $rootDir, Recipe $recipe, $vars): ?string { if (0 === \count($vars)) { return null; } $file = $rootDir.'/.gitignore'; $originalContents = file_exists($file) ? file_get_contents($file) : null; $this->configureGitignore( $recipe, $vars, true ); $updatedContents = file_exists($file) ? file_get_contents($file) : null; if (null === $originalContents) { if (file_exists($file)) { unlink($file); } } else { file_put_contents($file, $originalContents); } return $updatedContents; } } src/Configurator/MakefileConfigurator.php 0000644 00000007311 15120140521 0014577 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Configurator; use Symfony\Flex\Lock; use Symfony\Flex\Recipe; use Symfony\Flex\Update\RecipeUpdate; /** * @author Fabien Potencier <fabien@symfony.com> */ class MakefileConfigurator extends AbstractConfigurator { public function configure(Recipe $recipe, $definitions, Lock $lock, array $options = []) { $this->write('Adding Makefile entries'); $this->configureMakefile($recipe, $definitions, $options['force'] ?? false); } public function unconfigure(Recipe $recipe, $vars, Lock $lock) { if (!file_exists($makefile = $this->options->get('root-dir').'/Makefile')) { return; } $contents = preg_replace(sprintf('{%s*###> %s ###.*###< %s ###%s+}s', "\n", $recipe->getName(), $recipe->getName(), "\n"), "\n", file_get_contents($makefile), -1, $count); if (!$count) { return; } $this->write(sprintf('Removing Makefile entries from %s', $makefile)); if (!trim($contents)) { @unlink($makefile); } else { file_put_contents($makefile, ltrim($contents, "\r\n")); } } public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void { $recipeUpdate->setOriginalFile( 'Makefile', $this->getContentsAfterApplyingRecipe($recipeUpdate->getRootDir(), $recipeUpdate->getOriginalRecipe(), $originalConfig) ); $recipeUpdate->setNewFile( 'Makefile', $this->getContentsAfterApplyingRecipe($recipeUpdate->getRootDir(), $recipeUpdate->getNewRecipe(), $newConfig) ); } private function configureMakefile(Recipe $recipe, array $definitions, bool $update) { $makefile = $this->options->get('root-dir').'/Makefile'; if (!$update && $this->isFileMarked($recipe, $makefile)) { return; } $data = $this->options->expandTargetDir(implode("\n", $definitions)); $data = $this->markData($recipe, $data); $data = "\n".ltrim($data, "\r\n"); if (!file_exists($makefile)) { $envKey = $this->options->get('runtime')['env_var_name'] ?? 'APP_ENV'; $dotenvPath = $this->options->get('runtime')['dotenv_path'] ?? '.env'; file_put_contents( $this->options->get('root-dir').'/Makefile', <<<EOF ifndef {$envKey} include {$dotenvPath} endif .DEFAULT_GOAL := help .PHONY: help help: @awk 'BEGIN {FS = ":.*?## "}; /^[a-zA-Z-]+:.*?## .*$$/ {printf "\033[32m%-15s\033[0m %s\\n", $$1, $$2}' Makefile | sort EOF ); } if (!$this->updateData($makefile, $data)) { file_put_contents($makefile, $data, \FILE_APPEND); } } private function getContentsAfterApplyingRecipe(string $rootDir, Recipe $recipe, array $definitions): ?string { if (0 === \count($definitions)) { return null; } $file = $rootDir.'/Makefile'; $originalContents = file_exists($file) ? file_get_contents($file) : null; $this->configureMakefile( $recipe, $definitions, true ); $updatedContents = file_exists($file) ? file_get_contents($file) : null; if (null === $originalContents) { if (file_exists($file)) { unlink($file); } } else { file_put_contents($file, $originalContents); } return $updatedContents; } } src/Event/UpdateEvent.php 0000644 00000001351 15120140521 0011340 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Event; use Composer\Script\Event; use Composer\Script\ScriptEvents; class UpdateEvent extends Event { private $force; private $reset; public function __construct(bool $force, bool $reset) { $this->name = ScriptEvents::POST_UPDATE_CMD; $this->force = $force; $this->reset = $reset; } public function force(): bool { return $this->force; } public function reset(): bool { return $this->reset; } } src/Unpack/Operation.php 0000644 00000001677 15120140521 0011227 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Unpack; class Operation { private $packages = []; private $unpack; private $sort; public function __construct(bool $unpack, bool $sort) { $this->unpack = $unpack; $this->sort = $sort; } public function addPackage(string $name, string $version, bool $dev) { $this->packages[] = [ 'name' => $name, 'version' => $version, 'dev' => $dev, ]; } public function getPackages(): array { return $this->packages; } public function shouldUnpack(): bool { return $this->unpack; } public function shouldSort(): bool { return $this->sort; } } src/Unpack/Result.php 0000644 00000002122 15120140521 0010527 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Unpack; use Composer\Package\PackageInterface; class Result { private $unpacked = []; private $required = []; public function addUnpacked(PackageInterface $package): bool { $name = $package->getName(); if (!isset($this->unpacked[$name])) { $this->unpacked[$name] = $package; return true; } return false; } /** * @return PackageInterface[] */ public function getUnpacked(): array { return $this->unpacked; } public function addRequired(string $package) { $this->required[] = $package; } /** * @return string[] */ public function getRequired(): array { // we need at least one package for the command to work properly return $this->required ?: ['symfony/flex']; } } src/Update/DiffHelper.php 0000644 00000002620 15120140521 0011265 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Update; class DiffHelper { public static function removeFilesFromPatch(string $patch, array $files, array &$removedPatches): string { foreach ($files as $filename) { $start = strpos($patch, sprintf('diff --git a/%s b/%s', $filename, $filename)); if (false === $start) { throw new \LogicException(sprintf('Could not find file "%s" in the patch.', $filename)); } $end = strpos($patch, 'diff --git a/', $start + 1); $contentBefore = substr($patch, 0, $start); if (false === $end) { // last patch in the file $removedPatches[$filename] = rtrim(substr($patch, $start), "\n"); $patch = rtrim($contentBefore, "\n"); continue; } $removedPatches[$filename] = rtrim(substr($patch, $start, $end - $start), "\n"); $patch = $contentBefore.substr($patch, $end); } // valid patches end with a blank line if ($patch && "\n" !== substr($patch, \strlen($patch) - 1, 1)) { $patch = $patch."\n"; } return $patch; } } src/Update/RecipePatch.php 0000644 00000002200 15120140521 0011436 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Update; class RecipePatch { private $patch; private $blobs; private $deletedFiles; private $removedPatches; public function __construct(string $patch, array $blobs, array $deletedFiles, array $removedPatches = []) { $this->patch = $patch; $this->blobs = $blobs; $this->deletedFiles = $deletedFiles; $this->removedPatches = $removedPatches; } public function getPatch(): string { return $this->patch; } public function getBlobs(): array { return $this->blobs; } public function getDeletedFiles(): array { return $this->deletedFiles; } /** * Patches for modified files that were removed because the file * has been deleted in the user's project. */ public function getRemovedPatches(): array { return $this->removedPatches; } } src/Update/RecipePatcher.php 0000644 00000022041 15120140521 0011772 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Update; use Composer\IO\IOInterface; use Composer\Util\ProcessExecutor; use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Filesystem; class RecipePatcher { private $rootDir; private $filesystem; private $io; private $processExecutor; public function __construct(string $rootDir, IOInterface $io) { $this->rootDir = $rootDir; $this->filesystem = new Filesystem(); $this->io = $io; $this->processExecutor = new ProcessExecutor($io); } /** * Applies the patch. If it fails unexpectedly, an exception will be thrown. * * @return bool returns true if fully successful, false if conflicts were encountered */ public function applyPatch(RecipePatch $patch): bool { $withConflicts = $this->_applyPatchFile($patch); foreach ($patch->getDeletedFiles() as $deletedFile) { if (file_exists($this->rootDir.'/'.$deletedFile)) { $this->execute(sprintf('git rm %s', ProcessExecutor::escape($deletedFile)), $this->rootDir); } } return $withConflicts; } public function generatePatch(array $originalFiles, array $newFiles): RecipePatch { $ignoredFiles = $this->getIgnoredFiles(array_keys($originalFiles) + array_keys($newFiles)); // null implies "file does not exist" $originalFiles = array_filter($originalFiles, function ($file, $fileName) use ($ignoredFiles) { return null !== $file && !\in_array($fileName, $ignoredFiles); }, \ARRAY_FILTER_USE_BOTH); $newFiles = array_filter($newFiles, function ($file, $fileName) use ($ignoredFiles) { return null !== $file && !\in_array($fileName, $ignoredFiles); }, \ARRAY_FILTER_USE_BOTH); $deletedFiles = []; // find removed files & record that they are deleted // unset them from originalFiles to avoid unnecessary blobs being added foreach ($originalFiles as $file => $contents) { if (!isset($newFiles[$file])) { $deletedFiles[] = $file; unset($originalFiles[$file]); } } // If a file is being modified, but does not exist in the current project, // it cannot be patched. We generate the diff for these, but then remove // it from the patch (and optionally report this diff to the user). $modifiedFiles = array_intersect_key(array_keys($originalFiles), array_keys($newFiles)); $deletedModifiedFiles = []; foreach ($modifiedFiles as $modifiedFile) { if (!file_exists($this->rootDir.'/'.$modifiedFile) && $originalFiles[$modifiedFile] !== $newFiles[$modifiedFile]) { $deletedModifiedFiles[] = $modifiedFile; } } // Use git binary to get project path from repository root $prefix = trim($this->execute('git rev-parse --show-prefix', $this->rootDir)); $tmpPath = sys_get_temp_dir().'/_flex_recipe_update'.uniqid(mt_rand(), true); $this->filesystem->mkdir($tmpPath); try { $this->execute('git init', $tmpPath); $this->execute('git config commit.gpgsign false', $tmpPath); $this->execute('git config user.name "Flex Updater"', $tmpPath); $this->execute('git config user.email ""', $tmpPath); $blobs = []; if (\count($originalFiles) > 0) { $this->writeFiles($originalFiles, $tmpPath); $this->execute('git add -A', $tmpPath); $this->execute('git commit -m "original files"', $tmpPath); $blobs = $this->generateBlobs($originalFiles, $tmpPath); } $this->writeFiles($newFiles, $tmpPath); $this->execute('git add -A', $tmpPath); $patchString = $this->execute(sprintf('git diff --cached --src-prefix "a/%s" --dst-prefix "b/%s"', $prefix, $prefix), $tmpPath); $removedPatches = []; $patchString = DiffHelper::removeFilesFromPatch($patchString, $deletedModifiedFiles, $removedPatches); return new RecipePatch( $patchString, $blobs, $deletedFiles, $removedPatches ); } finally { try { $this->filesystem->remove($tmpPath); } catch (IOException $e) { // this can sometimes fail due to git file permissions // if that happens, just leave it: we're in the temp directory anyways } } } private function writeFiles(array $files, string $directory): void { foreach ($files as $filename => $contents) { $path = $directory.'/'.$filename; if (null === $contents) { if (file_exists($path)) { unlink($path); } continue; } if (!file_exists(\dirname($path))) { $this->filesystem->mkdir(\dirname($path)); } file_put_contents($path, $contents); } } private function execute(string $command, string $cwd): string { $output = ''; $statusCode = $this->processExecutor->execute($command, $output, $cwd); if (0 !== $statusCode) { throw new \LogicException(sprintf('Command "%s" failed: "%s". Output: "%s".', $command, $this->processExecutor->getErrorOutput(), $output)); } return $output; } /** * Adds git blobs for each original file. * * For patching to work, each original file & contents needs to be * available to git as a blob. This is because the patch contains * the ref to the original blob, and git uses that to find the * original file (which is needed for the 3-way merge). */ private function addMissingBlobs(array $blobs): array { $addedBlobs = []; foreach ($blobs as $hash => $contents) { $blobPath = $this->getBlobPath($this->rootDir, $hash); if (file_exists($blobPath)) { continue; } $addedBlobs[] = $blobPath; if (!file_exists(\dirname($blobPath))) { $this->filesystem->mkdir(\dirname($blobPath)); } file_put_contents($blobPath, $contents); } return $addedBlobs; } private function generateBlobs(array $originalFiles, string $originalFilesRoot): array { $addedBlobs = []; foreach ($originalFiles as $filename => $contents) { // if the file didn't originally exist, no blob needed if (!file_exists($originalFilesRoot.'/'.$filename)) { continue; } $hash = trim($this->execute('git hash-object '.ProcessExecutor::escape($filename), $originalFilesRoot)); $addedBlobs[$hash] = file_get_contents($this->getBlobPath($originalFilesRoot, $hash)); } return $addedBlobs; } private function getBlobPath(string $gitRoot, string $hash): string { $gitDir = trim($this->execute('git rev-parse --absolute-git-dir', $gitRoot)); $hashStart = substr($hash, 0, 2); $hashEnd = substr($hash, 2); return $gitDir.'/objects/'.$hashStart.'/'.$hashEnd; } private function _applyPatchFile(RecipePatch $patch) { if (!$patch->getPatch()) { // nothing to do! return true; } $addedBlobs = $this->addMissingBlobs($patch->getBlobs()); $patchPath = $this->rootDir.'/_flex_recipe_update.patch'; file_put_contents($patchPath, $patch->getPatch()); try { $this->execute('git update-index --refresh', $this->rootDir); $output = ''; $statusCode = $this->processExecutor->execute('git apply "_flex_recipe_update.patch" -3', $output, $this->rootDir); if (0 === $statusCode) { // successful with no conflicts return true; } if (false !== strpos($this->processExecutor->getErrorOutput(), 'with conflicts')) { // successful with conflicts return false; } throw new \LogicException('Error applying the patch: '.$this->processExecutor->getErrorOutput()); } finally { unlink($patchPath); // clean up any temporary blobs foreach ($addedBlobs as $filename) { unlink($filename); } } } private function getIgnoredFiles(array $fileNames): array { $args = implode(' ', array_map([ProcessExecutor::class, 'escape'], $fileNames)); $output = ''; $this->processExecutor->execute(sprintf('git check-ignore %s', $args), $output, $this->rootDir); return $this->processExecutor->splitLines($output); } } src/Update/RecipeUpdate.php 0000644 00000005010 15120140521 0011623 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex\Update; use Symfony\Flex\Lock; use Symfony\Flex\Recipe; class RecipeUpdate { private $originalRecipe; private $newRecipe; private $lock; private $rootDir; /** @var string[] */ private $originalRecipeFiles = []; /** @var string[] */ private $newRecipeFiles = []; private $copyFromPackagePaths = []; public function __construct(Recipe $originalRecipe, Recipe $newRecipe, Lock $lock, string $rootDir) { $this->originalRecipe = $originalRecipe; $this->newRecipe = $newRecipe; $this->lock = $lock; $this->rootDir = $rootDir; } public function getOriginalRecipe(): Recipe { return $this->originalRecipe; } public function getNewRecipe(): Recipe { return $this->newRecipe; } public function getLock(): Lock { return $this->lock; } public function getRootDir(): string { return $this->rootDir; } public function getPackageName(): string { return $this->originalRecipe->getName(); } public function setOriginalFile(string $filename, ?string $contents): void { $this->originalRecipeFiles[$filename] = $contents; } public function setNewFile(string $filename, ?string $contents): void { $this->newRecipeFiles[$filename] = $contents; } public function addOriginalFiles(array $files) { foreach ($files as $file => $contents) { if (null === $contents) { continue; } $this->setOriginalFile($file, $contents); } } public function addNewFiles(array $files) { foreach ($files as $file => $contents) { if (null === $contents) { continue; } $this->setNewFile($file, $contents); } } public function getOriginalFiles(): array { return $this->originalRecipeFiles; } public function getNewFiles(): array { return $this->newRecipeFiles; } public function getCopyFromPackagePaths(): array { return $this->copyFromPackagePaths; } public function addCopyFromPackagePath(string $source, string $target) { $this->copyFromPackagePaths[$source] = $target; } } src/Cache.php 0000644 00000012432 15120140521 0007040 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\Cache as BaseCache; use Composer\IO\IOInterface; use Composer\Package\RootPackageInterface; use Composer\Semver\Constraint\Constraint; use Composer\Semver\VersionParser; /** * @author Nicolas Grekas <p@tchwork.com> */ class Cache extends BaseCache { private $versions; private $versionParser; private $symfonyRequire; private $rootConstraints = []; private $symfonyConstraints; private $downloader; private $io; public function setSymfonyRequire(string $symfonyRequire, RootPackageInterface $rootPackage, Downloader $downloader, IOInterface $io = null) { $this->versionParser = new VersionParser(); $this->symfonyRequire = $symfonyRequire; $this->symfonyConstraints = $this->versionParser->parseConstraints($symfonyRequire); $this->downloader = $downloader; $this->io = $io; foreach ($rootPackage->getRequires() + $rootPackage->getDevRequires() as $name => $link) { $this->rootConstraints[$name] = $link->getConstraint(); } } public function read($file) { $content = parent::read($file); if (0 === strpos($file, 'provider-symfony$') && \is_array($data = json_decode($content, true))) { $content = json_encode($this->removeLegacyTags($data)); } return $content; } public function removeLegacyTags(array $data): array { if (!$this->symfonyConstraints || !isset($data['packages'])) { return $data; } foreach ($data['packages'] as $name => $versions) { if (!isset($this->getVersions()['splits'][$name])) { continue; } $rootConstraint = $this->rootConstraints[$name] ?? null; $rootVersions = []; foreach ($versions as $version => $composerJson) { if (null !== $alias = $composerJson['extra']['branch-alias'][$version] ?? null) { $normalizedVersion = $this->versionParser->normalize($alias); } elseif (null === $normalizedVersion = $composerJson['version_normalized'] ?? null) { continue; } $constraint = new Constraint('==', $normalizedVersion); if ($rootConstraint && $rootConstraint->matches($constraint)) { $rootVersions[$version] = $composerJson; } if (!$this->symfonyConstraints->matches($constraint)) { if (null !== $this->io) { $this->io->writeError(sprintf('<info>Restricting packages listed in "symfony/symfony" to "%s"</>', $this->symfonyRequire)); $this->io = null; } unset($versions[$version]); } } if ($rootConstraint && !array_intersect_key($rootVersions, $versions)) { $versions = $rootVersions; } $data['packages'][$name] = $versions; } if (null === $symfonySymfony = $data['packages']['symfony/symfony'] ?? null) { return $data; } foreach ($symfonySymfony as $version => $composerJson) { if (null !== $alias = $composerJson['extra']['branch-alias'][$version] ?? null) { $normalizedVersion = $this->versionParser->normalize($alias); } elseif (null === $normalizedVersion = $composerJson['version_normalized'] ?? null) { continue; } if (!$this->symfonyConstraints->matches(new Constraint('==', $normalizedVersion))) { unset($symfonySymfony[$version]); } } if ($symfonySymfony) { $data['packages']['symfony/symfony'] = $symfonySymfony; } return $data; } private function getVersions(): array { if (null !== $this->versions) { return $this->versions; } $versions = $this->downloader->getVersions(); $this->downloader = null; $okVersions = []; if (!isset($versions['splits'])) { throw new \LogicException('The Flex index is missing a "splits" entry. Did you forget to add "flex://defaults" in the "extra.symfony.endpoint" array of your composer.json?'); } foreach ($versions['splits'] as $name => $vers) { foreach ($vers as $i => $v) { if (!isset($okVersions[$v])) { $okVersions[$v] = false; for ($j = 0; $j < 60; ++$j) { if ($this->symfonyConstraints->matches(new Constraint('==', $v.'.'.$j.'.0'))) { $okVersions[$v] = true; break; } } } if (!$okVersions[$v]) { unset($vers[$i]); } } if (!$vers || $vers === $versions['splits'][$name]) { unset($versions['splits'][$name]); } } return $this->versions = $versions; } } src/ComposerRepository.php 0000644 00000002761 15120140521 0011730 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\Repository\ComposerRepository as BaseComposerRepository; /** * @author Nicolas Grekas <p@tchwork.com> */ class ComposerRepository extends BaseComposerRepository { private $providerFiles; protected function loadProviderListings($data) { if (null !== $this->providerFiles) { parent::loadProviderListings($data); return; } $data = [$data]; while ($data) { $this->providerFiles = []; foreach ($data as $data) { $this->loadProviderListings($data); } $loadingFiles = $this->providerFiles; $this->providerFiles = null; $data = []; $this->rfs->download($loadingFiles, function (...$args) use (&$data) { $data[] = $this->fetchFile(...$args); }); } } protected function fetchFile($filename, $cacheKey = null, $sha256 = null, $storeLastModifiedTime = false) { if (null !== $this->providerFiles) { $this->providerFiles[] = [$filename, $cacheKey, $sha256, $storeLastModifiedTime]; return []; } return parent::fetchFile($filename, $cacheKey, $sha256, $storeLastModifiedTime); } } src/Configurator.php 0000644 00000010156 15120140521 0010500 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\Composer; use Composer\IO\IOInterface; use Symfony\Flex\Configurator\AbstractConfigurator; use Symfony\Flex\Update\RecipeUpdate; /** * @author Fabien Potencier <fabien@symfony.com> */ class Configurator { private $composer; private $io; private $options; private $configurators; private $postInstallConfigurators; private $cache; public function __construct(Composer $composer, IOInterface $io, Options $options) { $this->composer = $composer; $this->io = $io; $this->options = $options; // ordered list of configurators $this->configurators = [ 'bundles' => Configurator\BundlesConfigurator::class, 'copy-from-recipe' => Configurator\CopyFromRecipeConfigurator::class, 'copy-from-package' => Configurator\CopyFromPackageConfigurator::class, 'env' => Configurator\EnvConfigurator::class, 'container' => Configurator\ContainerConfigurator::class, 'makefile' => Configurator\MakefileConfigurator::class, 'composer-scripts' => Configurator\ComposerScriptsConfigurator::class, 'gitignore' => Configurator\GitignoreConfigurator::class, 'dockerfile' => Configurator\DockerfileConfigurator::class, 'docker-compose' => Configurator\DockerComposeConfigurator::class, ]; $this->postInstallConfigurators = [ 'add-lines' => Configurator\AddLinesConfigurator::class, ]; } public function install(Recipe $recipe, Lock $lock, array $options = []) { $manifest = $recipe->getManifest(); foreach (array_keys($this->configurators) as $key) { if (isset($manifest[$key])) { $this->get($key)->configure($recipe, $manifest[$key], $lock, $options); } } } /** * Run after all recipes have been installed to run post-install configurators. */ public function postInstall(Recipe $recipe, Lock $lock, array $options = []) { $manifest = $recipe->getManifest(); foreach (array_keys($this->postInstallConfigurators) as $key) { if (isset($manifest[$key])) { $this->get($key)->configure($recipe, $manifest[$key], $lock, $options); } } } public function populateUpdate(RecipeUpdate $recipeUpdate): void { $originalManifest = $recipeUpdate->getOriginalRecipe()->getManifest(); $newManifest = $recipeUpdate->getNewRecipe()->getManifest(); $allConfigurators = array_merge($this->configurators, $this->postInstallConfigurators); foreach (array_keys($allConfigurators) as $key) { if (!isset($originalManifest[$key]) && !isset($newManifest[$key])) { continue; } $this->get($key)->update($recipeUpdate, $originalManifest[$key] ?? [], $newManifest[$key] ?? []); } } public function unconfigure(Recipe $recipe, Lock $lock) { $manifest = $recipe->getManifest(); $allConfigurators = array_merge($this->configurators, $this->postInstallConfigurators); foreach (array_keys($allConfigurators) as $key) { if (isset($manifest[$key])) { $this->get($key)->unconfigure($recipe, $manifest[$key], $lock); } } } private function get($key): AbstractConfigurator { if (!isset($this->configurators[$key]) && !isset($this->postInstallConfigurators[$key])) { throw new \InvalidArgumentException(sprintf('Unknown configurator "%s".', $key)); } if (isset($this->cache[$key])) { return $this->cache[$key]; } $class = isset($this->configurators[$key]) ? $this->configurators[$key] : $this->postInstallConfigurators[$key]; return $this->cache[$key] = new $class($this->composer, $this->io, $this->options); } } src/CurlDownloader.php 0000644 00000020773 15120140521 0010770 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\Downloader\TransportException; /** * @author Nicolas Grekas <p@tchwork.com> */ class CurlDownloader { private $multiHandle; private $shareHandle; private $jobs = []; private $exceptions = []; private static $options = [ 'http' => [ 'method' => \CURLOPT_CUSTOMREQUEST, 'content' => \CURLOPT_POSTFIELDS, ], 'ssl' => [ 'cafile' => \CURLOPT_CAINFO, 'capath' => \CURLOPT_CAPATH, ], ]; private static $timeInfo = [ 'total_time' => true, 'namelookup_time' => true, 'connect_time' => true, 'pretransfer_time' => true, 'starttransfer_time' => true, 'redirect_time' => true, ]; public function __construct() { $this->multiHandle = $mh = curl_multi_init(); curl_multi_setopt($mh, \CURLMOPT_PIPELINING, /*CURLPIPE_MULTIPLEX*/ 2); if (\defined('CURLMOPT_MAX_HOST_CONNECTIONS')) { curl_multi_setopt($mh, \CURLMOPT_MAX_HOST_CONNECTIONS, 8); } $this->shareHandle = $sh = curl_share_init(); curl_share_setopt($sh, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_COOKIE); curl_share_setopt($sh, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_DNS); curl_share_setopt($sh, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_SSL_SESSION); } public function get($origin, $url, $context, $file) { $params = stream_context_get_params($context); $ch = curl_init(); $hd = fopen('php://temp/maxmemory:32768', 'w+b'); if ($file && !$fd = @fopen($file.'~', 'w+b')) { $file = null; } if (!$file) { $fd = @fopen('php://temp/maxmemory:524288', 'w+b'); } $headers = array_diff($params['options']['http']['header'], ['Connection: close']); if (!isset($params['options']['http']['protocol_version'])) { curl_setopt($ch, \CURLOPT_HTTP_VERSION, \CURL_HTTP_VERSION_1_0); } else { $headers[] = 'Connection: keep-alive'; if (0 === strpos($url, 'https://') && \defined('CURL_VERSION_HTTP2') && \defined('CURL_HTTP_VERSION_2_0') && (\CURL_VERSION_HTTP2 & curl_version()['features'])) { curl_setopt($ch, \CURLOPT_HTTP_VERSION, \CURL_HTTP_VERSION_2_0); } } curl_setopt($ch, \CURLOPT_URL, $url); curl_setopt($ch, \CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, \CURLOPT_DNS_USE_GLOBAL_CACHE, false); curl_setopt($ch, \CURLOPT_WRITEHEADER, $hd); curl_setopt($ch, \CURLOPT_FILE, $fd); curl_setopt($ch, \CURLOPT_SHARE, $this->shareHandle); foreach (self::$options as $type => $options) { foreach ($options as $name => $curlopt) { if (isset($params['options'][$type][$name])) { curl_setopt($ch, $curlopt, $params['options'][$type][$name]); } } } $progress = array_diff_key(curl_getinfo($ch), self::$timeInfo); $this->jobs[(int) $ch] = [ 'progress' => $progress, 'ch' => $ch, 'callback' => $params['notification'], 'file' => $file, 'fd' => $fd, ]; curl_multi_add_handle($this->multiHandle, $ch); $params['notification'](\STREAM_NOTIFY_RESOLVE, \STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0, false); $active = true; try { while ($active && isset($this->jobs[(int) $ch])) { curl_multi_exec($this->multiHandle, $active); curl_multi_select($this->multiHandle); while ($progress = curl_multi_info_read($this->multiHandle)) { if (!isset($this->jobs[$i = (int) $h = $progress['handle']])) { continue; } $progress = array_diff_key(curl_getinfo($h), self::$timeInfo); $job = $this->jobs[$i]; unset($this->jobs[$i]); curl_multi_remove_handle($this->multiHandle, $h); try { $this->onProgress($h, $job['callback'], $progress, $job['progress']); if ('' !== curl_error($h)) { throw new TransportException(curl_error($h)); } if ($job['file'] && \CURLE_OK === curl_errno($h) && !isset($this->exceptions[$i])) { fclose($job['fd']); rename($job['file'].'~', $job['file']); } } catch (TransportException $e) { $this->exceptions[$i] = $e; } } foreach ($this->jobs as $i => $h) { if (!isset($this->jobs[$i])) { continue; } $h = $this->jobs[$i]['ch']; $progress = array_diff_key(curl_getinfo($h), self::$timeInfo); if ($this->jobs[$i]['progress'] !== $progress) { $previousProgress = $this->jobs[$i]['progress']; $this->jobs[$i]['progress'] = $progress; try { $this->onProgress($h, $this->jobs[$i]['callback'], $progress, $previousProgress); } catch (TransportException $e) { unset($this->jobs[$i]); curl_multi_remove_handle($this->multiHandle, $h); $this->exceptions[$i] = $e; } } } } if ('' !== curl_error($ch) || \CURLE_OK !== curl_errno($ch)) { $this->exceptions[(int) $ch] = new TransportException(curl_error($ch), curl_getinfo($ch, \CURLINFO_HTTP_CODE) ?: 0); } if (isset($this->exceptions[(int) $ch])) { throw $this->exceptions[(int) $ch]; } } finally { if ($file && !isset($this->exceptions[(int) $ch])) { $fd = fopen($file, 'rb'); } $progress = array_diff_key(curl_getinfo($ch), self::$timeInfo); $this->finishProgress($ch, $params['notification'], $progress); unset($this->jobs[(int) $ch], $this->exceptions[(int) $ch]); curl_multi_remove_handle($this->multiHandle, $ch); curl_close($ch); rewind($hd); $headers = explode("\r\n", rtrim(stream_get_contents($hd))); fclose($hd); rewind($fd); $contents = stream_get_contents($fd); fclose($fd); } return [$headers, $contents]; } private function onProgress($ch, callable $notify, array $progress, array $previousProgress) { if (300 <= $progress['http_code'] && $progress['http_code'] < 400 || 0 > $progress['download_content_length']) { return; } if (!$previousProgress['http_code'] && $progress['http_code'] && $progress['http_code'] < 200 || 400 <= $progress['http_code']) { $code = 403 === $progress['http_code'] ? \STREAM_NOTIFY_AUTH_RESULT : \STREAM_NOTIFY_FAILURE; $notify($code, \STREAM_NOTIFY_SEVERITY_ERR, curl_error($ch), $progress['http_code'], 0, 0, false); } if ($previousProgress['download_content_length'] < $progress['download_content_length']) { $notify(\STREAM_NOTIFY_FILE_SIZE_IS, \STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, (int) $progress['download_content_length'], false); } if ($previousProgress['size_download'] < $progress['size_download']) { $notify(\STREAM_NOTIFY_PROGRESS, \STREAM_NOTIFY_SEVERITY_INFO, '', 0, (int) $progress['size_download'], (int) $progress['download_content_length'], false); } } private function finishProgress($ch, callable $notify, array $progress) { if ($progress['download_content_length'] < 0) { $notify(\STREAM_NOTIFY_FILE_SIZE_IS, \STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, (int) $progress['size_download'], false); $notify(\STREAM_NOTIFY_PROGRESS, \STREAM_NOTIFY_SEVERITY_INFO, '', 0, (int) $progress['size_download'], (int) $progress['size_download'], false); } } } src/Downloader.php 0000644 00000045012 15120140521 0010133 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\Cache as ComposerCache; use Composer\Composer; use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Util\Http\Response as ComposerResponse; use Composer\Util\HttpDownloader; use Composer\Util\Loop; /** * @author Fabien Potencier <fabien@symfony.com> * @author Nicolas Grekas <p@tchwork.com> */ class Downloader { private const DEFAULT_ENDPOINTS = [ 'https://raw.githubusercontent.com/symfony/recipes/flex/main/index.json', 'https://raw.githubusercontent.com/symfony/recipes-contrib/flex/main/index.json', ]; private const MAX_LENGTH = 1000; private static $versions; private static $aliases; private $io; private $sess; private $cache; /** @var HttpDownloader|ParallelDownloader */ private $rfs; private $degradedMode = false; private $endpoints; private $index; private $conflicts; private $legacyEndpoint; private $caFile; private $enabled = true; private $composer; public function __construct(Composer $composer, IoInterface $io, $rfs) { if (getenv('SYMFONY_CAFILE')) { $this->caFile = getenv('SYMFONY_CAFILE'); } if (null === $endpoint = $composer->getPackage()->getExtra()['symfony']['endpoint'] ?? null) { $this->endpoints = self::DEFAULT_ENDPOINTS; } elseif (\is_array($endpoint) || false !== strpos($endpoint, '.json') || 'flex://defaults' === $endpoint) { $this->endpoints = array_values((array) $endpoint); if (\is_string($endpoint) && false !== strpos($endpoint, '.json')) { $this->endpoints[] = 'flex://defaults'; } } else { $this->legacyEndpoint = rtrim($endpoint, '/'); } if (false === $endpoint = getenv('SYMFONY_ENDPOINT')) { // no-op } elseif (false !== strpos($endpoint, '.json') || 'flex://defaults' === $endpoint) { $this->endpoints ?? $this->endpoints = self::DEFAULT_ENDPOINTS; array_unshift($this->endpoints, $endpoint); $this->legacyEndpoint = null; } else { $this->endpoints = null; $this->legacyEndpoint = rtrim($endpoint, '/'); } if (null !== $this->endpoints) { if (false !== $i = array_search('flex://defaults', $this->endpoints, true)) { array_splice($this->endpoints, $i, 1, self::DEFAULT_ENDPOINTS); } $this->endpoints = array_fill_keys($this->endpoints, []); } $this->io = $io; $config = $composer->getConfig(); $this->rfs = $rfs; $this->cache = new ComposerCache($io, $config->get('cache-repo-dir').'/flex'); $this->sess = bin2hex(random_bytes(16)); $this->composer = $composer; } public function getSessionId(): string { return $this->sess; } public function setFlexId(string $id = null) { // No-op to support downgrading to v1.12.x } public function isEnabled() { return $this->enabled; } public function disable() { $this->enabled = false; } public function getVersions() { $this->initialize(); return self::$versions ?? self::$versions = current($this->get([$this->legacyEndpoint.'/versions.json'])); } public function getAliases() { $this->initialize(); return self::$aliases ?? self::$aliases = current($this->get([$this->legacyEndpoint.'/aliases.json'])); } /** * Downloads recipes. * * @param OperationInterface[] $operations */ public function getRecipes(array $operations): array { $this->initialize(); if ($this->conflicts) { $lockedRepository = $this->composer->getLocker()->getLockedRepository(); foreach ($this->conflicts as $conflicts) { foreach ($conflicts as $package => $versions) { foreach ($versions as $version => $conflicts) { foreach ($conflicts as $conflictingPackage => $constraint) { if ($lockedRepository->findPackage($conflictingPackage, $constraint)) { unset($this->index[$package][$version]); } } } } } $this->conflicts = []; } $data = []; $urls = []; $chunk = ''; $recipeRef = null; foreach ($operations as $operation) { $o = 'i'; if ($operation instanceof UpdateOperation) { $package = $operation->getTargetPackage(); $o = 'u'; } else { $package = $operation->getPackage(); if ($operation instanceof UninstallOperation) { $o = 'r'; } if ($operation instanceof InformationOperation) { $recipeRef = $operation->getRecipeRef(); } } $version = $package->getPrettyVersion(); if ($operation instanceof InformationOperation && $operation->getVersion()) { $version = $operation->getVersion(); } if (0 === strpos($version, 'dev-') && isset($package->getExtra()['branch-alias'])) { $branchAliases = $package->getExtra()['branch-alias']; if ( (isset($branchAliases[$version]) && $alias = $branchAliases[$version]) || (isset($branchAliases['dev-main']) && $alias = $branchAliases['dev-main']) || (isset($branchAliases['dev-trunk']) && $alias = $branchAliases['dev-trunk']) || (isset($branchAliases['dev-develop']) && $alias = $branchAliases['dev-develop']) || (isset($branchAliases['dev-default']) && $alias = $branchAliases['dev-default']) || (isset($branchAliases['dev-latest']) && $alias = $branchAliases['dev-latest']) || (isset($branchAliases['dev-next']) && $alias = $branchAliases['dev-next']) || (isset($branchAliases['dev-current']) && $alias = $branchAliases['dev-current']) || (isset($branchAliases['dev-support']) && $alias = $branchAliases['dev-support']) || (isset($branchAliases['dev-tip']) && $alias = $branchAliases['dev-tip']) || (isset($branchAliases['dev-master']) && $alias = $branchAliases['dev-master']) ) { $version = $alias; } } if ($recipeVersions = $this->index[$package->getName()] ?? null) { $version = explode('.', preg_replace('/^dev-|^v|\.x-dev$|-dev$/', '', $version)); $version = $version[0].'.'.($version[1] ?? '9999999'); foreach (array_reverse($recipeVersions) as $v => $endpoint) { if (version_compare($version, $v, '<')) { continue; } $data['locks'][$package->getName()]['version'] = $version; $data['locks'][$package->getName()]['recipe']['version'] = $v; $links = $this->endpoints[$endpoint]['_links']; if (null !== $recipeRef && isset($links['archived_recipes_template'])) { if (isset($links['archived_recipes_template_relative'])) { $links['archived_recipes_template'] = preg_replace('{[^/\?]*+(?=\?|$)}', $links['archived_recipes_template_relative'], $endpoint, 1); } $urls[] = strtr($links['archived_recipes_template'], [ '{package_dotted}' => str_replace('/', '.', $package->getName()), '{ref}' => $recipeRef, ]); break; } if (isset($links['recipe_template_relative'])) { $links['recipe_template'] = preg_replace('{[^/\?]*+(?=\?|$)}', $links['recipe_template_relative'], $endpoint, 1); } $urls[] = strtr($links['recipe_template'], [ '{package_dotted}' => str_replace('/', '.', $package->getName()), '{package}' => $package->getName(), '{version}' => $v, ]); break; } continue; } if (\is_array($recipeVersions)) { $data['conflicts'][$package->getName()] = true; } if (null !== $this->endpoints) { continue; } // FIXME: Multi name with getNames() $name = str_replace('/', ',', $package->getName()); $path = sprintf('%s,%s%s', $name, $o, $version); if ($date = $package->getReleaseDate()) { $path .= ','.$date->format('U'); } if (\strlen($chunk) + \strlen($path) > self::MAX_LENGTH) { $urls[] = $this->legacyEndpoint.'/p/'.$chunk; $chunk = $path; } elseif ($chunk) { $chunk .= ';'.$path; } else { $chunk = $path; } } if ($chunk) { $urls[] = $this->legacyEndpoint.'/p/'.$chunk; } if (null === $this->endpoints) { foreach ($this->get($urls, true) as $body) { foreach ($body['manifests'] ?? [] as $name => $manifest) { $data['manifests'][$name] = $manifest; } foreach ($body['locks'] ?? [] as $name => $lock) { $data['locks'][$name] = $lock; } } } else { foreach ($this->get($urls, true) as $body) { foreach ($body['manifests'] ?? [] as $name => $manifest) { if (null === $version = $data['locks'][$name]['recipe']['version'] ?? null) { continue; } $endpoint = $this->endpoints[$this->index[$name][$version]]; $data['locks'][$name]['recipe'] = [ 'repo' => $endpoint['_links']['repository'], 'branch' => $endpoint['branch'], 'version' => $version, 'ref' => $manifest['ref'], ]; foreach ($manifest['files'] ?? [] as $i => $file) { $manifest['files'][$i]['contents'] = \is_array($file['contents']) ? implode("\n", $file['contents']) : base64_decode($file['contents']); } $data['manifests'][$name] = $manifest + [ 'repository' => $endpoint['_links']['repository'], 'package' => $name, 'version' => $version, 'origin' => strtr($endpoint['_links']['origin_template'], [ '{package}' => $name, '{version}' => $version, ]), 'is_contrib' => $endpoint['is_contrib'] ?? false, ]; } } } return $data; } /** * Used to "hide" a recipe version so that the next most-recent will be returned. * * This is used when resolving "conflicts". */ public function removeRecipeFromIndex(string $packageName, string $version) { unset($this->index[$packageName][$version]); } /** * Fetches and decodes JSON HTTP response bodies. */ private function get(array $urls, bool $isRecipe = false, int $try = 3): array { $responses = []; $retries = []; $options = []; foreach ($urls as $url) { $cacheKey = self::generateCacheKey($url); $headers = []; if (preg_match('{^https?://api\.github\.com/}', $url)) { $headers[] = 'Accept: application/vnd.github.v3.raw'; } elseif (preg_match('{^https?://raw\.githubusercontent\.com/}', $url) && $this->io->hasAuthentication('github.com')) { $auth = $this->io->getAuthentication('github.com'); if ('x-oauth-basic' === $auth['password']) { $headers[] = 'Authorization: token '.$auth['username']; } } elseif ($this->legacyEndpoint) { $headers[] = 'Package-Session: '.$this->sess; } if ($contents = $this->cache->read($cacheKey)) { $cachedResponse = Response::fromJson(json_decode($contents, true)); if ($lastModified = $cachedResponse->getHeader('last-modified')) { $headers[] = 'If-Modified-Since: '.$lastModified; } if ($eTag = $cachedResponse->getHeader('etag')) { $headers[] = 'If-None-Match: '.$eTag; } $responses[$url] = $cachedResponse->getBody(); } $options[$url] = $this->getOptions($headers); } if ($this->rfs instanceof HttpDownloader) { $loop = new Loop($this->rfs); $jobs = []; foreach ($urls as $url) { $jobs[] = $this->rfs->add($url, $options[$url])->then(function (ComposerResponse $response) use ($url, &$responses) { if (200 === $response->getStatusCode()) { $cacheKey = self::generateCacheKey($url); $responses[$url] = $this->parseJson($response->getBody(), $url, $cacheKey, $response->getHeaders())->getBody(); } }, function (\Exception $e) use ($url, &$retries) { $retries[] = [$url, $e]; }); } $loop->wait($jobs); } else { foreach ($urls as $i => $url) { $urls[$i] = [$url]; } $this->rfs->download($urls, function ($url) use ($options, &$responses, &$retries, &$error) { try { $cacheKey = self::generateCacheKey($url); $origin = method_exists($this->rfs, 'getOrigin') ? $this->rfs::getOrigin($url) : parse_url($url, \PHP_URL_HOST); $json = $this->rfs->getContents($origin, $url, false, $options[$url]); if (200 === $this->rfs->findStatusCode($this->rfs->getLastHeaders())) { $responses[$url] = $this->parseJson($json, $url, $cacheKey, $this->rfs->getLastHeaders())->getBody(); } } catch (\Exception $e) { $retries[] = [$url, $e]; } }); } if (!$retries) { return $responses; } if (0 < --$try) { usleep(100000); return $this->get(array_column($retries, 0), $isRecipe, $try) + $responses; } foreach ($retries as [$url, $e]) { if (isset($responses[$url])) { $this->switchToDegradedMode($e, $url); } elseif ($isRecipe) { $this->io->writeError('<warning>Failed to download recipe: '.$e->getMessage().'</>'); } else { throw $e; } } return $responses; } private function parseJson(string $json, string $url, string $cacheKey, array $lastHeaders): Response { $data = JsonFile::parseJson($json, $url); if (!empty($data['warning'])) { $this->io->writeError('<warning>Warning from '.$url.': '.$data['warning'].'</>'); } if (!empty($data['info'])) { $this->io->writeError('<info>Info from '.$url.': '.$data['info'].'</>'); } $response = new Response($data, $lastHeaders); if ($cacheKey && ($response->getHeader('last-modified') || $response->getHeader('etag'))) { $this->cache->write($cacheKey, json_encode($response)); } return $response; } private function switchToDegradedMode(\Exception $e, string $url) { if (!$this->degradedMode) { $this->io->writeError('<warning>'.$e->getMessage().'</>'); $this->io->writeError('<warning>'.$url.' could not be fully loaded, package information was loaded from the local cache and may be out of date</>'); } $this->degradedMode = true; } private function getOptions(array $headers): array { $options = ['http' => ['header' => $headers]]; if (null !== $this->caFile) { $options['ssl']['cafile'] = $this->caFile; } return $options; } private function initialize() { if (null !== $this->index || null === $this->endpoints) { $this->index ?? $this->index = []; return; } $indexes = self::$versions = self::$aliases = []; foreach ($this->get(array_keys($this->endpoints)) as $endpoint => $index) { $indexes[$endpoint] = $index; } foreach ($this->endpoints as $endpoint => $config) { $config = $indexes[$endpoint] ?? []; foreach ($config['recipes'] ?? [] as $package => $versions) { $this->index[$package] = $this->index[$package] ?? array_fill_keys($versions, $endpoint); } $this->conflicts[] = $config['recipe-conflicts'] ?? []; self::$versions += $config['versions'] ?? []; self::$aliases += $config['aliases'] ?? []; unset($config['recipes'], $config['recipe-conflicts'], $config['versions'], $config['aliases']); $this->endpoints[$endpoint] = $config; } } private static function generateCacheKey(string $url): string { $url = preg_replace('{^https://api.github.com/repos/([^/]++/[^/]++)/contents/}', '$1/', $url); $url = preg_replace('{^https://raw.githubusercontent.com/([^/]++/[^/]++)/}', '$1/', $url); $key = preg_replace('{[^a-z0-9.]}i', '-', $url); // eCryptfs can have problems with filenames longer than around 143 chars return \strlen($key) > 140 ? md5($url) : $key; } } src/Flex.php 0000644 00000127061 15120140521 0006740 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\Command\GlobalCommand; use Composer\Composer; use Composer\Console\Application; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Transaction; use Composer\Downloader\FileDownloader; use Composer\EventDispatcher\EventSubscriberInterface; use Composer\Factory; use Composer\Installer; use Composer\Installer\InstallerEvent; use Composer\Installer\InstallerEvents; use Composer\Installer\PackageEvent; use Composer\Installer\PackageEvents; use Composer\Installer\SuggestedPackagesReporter; use Composer\IO\IOInterface; use Composer\IO\NullIO; use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; use Composer\Package\BasePackage; use Composer\Package\Comparer\Comparer; use Composer\Package\Locker; use Composer\Package\Package; use Composer\Package\PackageInterface; use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginInterface; use Composer\Plugin\PreFileDownloadEvent; use Composer\Plugin\PrePoolCreateEvent; use Composer\Repository\ComposerRepository as BaseComposerRepository; use Composer\Repository\RepositoryFactory; use Composer\Repository\RepositoryManager; use Composer\Script\Event; use Composer\Script\ScriptEvents; use Composer\Semver\VersionParser; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Filesystem\Filesystem; use Symfony\Flex\Event\UpdateEvent; use Symfony\Flex\Unpack\Operation; use Symfony\Thanks\Thanks; /** * @author Fabien Potencier <fabien@symfony.com> * @author Nicolas Grekas <p@tchwork.com> */ class Flex implements PluginInterface, EventSubscriberInterface { /** * @var Composer */ private $composer; /** * @var IOInterface */ private $io; private $config; private $options; private $configurator; private $downloader; /** * @var Installer */ private $installer; private $postInstallOutput = ['']; private $operations = []; private $lock; private $cacheDirPopulated = false; private $displayThanksReminder = 0; private $rfs; private $progress = true; private $dryRun = false; private $reinstall; private static $activated = true; private static $repoReadingCommands = [ 'create-project' => true, 'outdated' => true, 'require' => true, 'update' => true, 'install' => true, ]; private static $aliasResolveCommands = [ 'require' => true, 'update' => false, 'remove' => false, 'unpack' => true, ]; private $filter; public function activate(Composer $composer, IOInterface $io) { if (!\extension_loaded('openssl')) { self::$activated = false; $io->writeError('<warning>Symfony Flex has been disabled. You must enable the openssl extension in your "php.ini" file.</>'); return; } // to avoid issues when Flex is upgraded, we load all PHP classes now // that way, we are sure to use all classes from the same version foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(__DIR__, \FilesystemIterator::SKIP_DOTS)) as $file) { if ('.php' === substr($file, -4)) { class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__), -4))); } } $this->composer = $composer; $this->io = $io; $this->config = $composer->getConfig(); $this->options = $this->initOptions(); $symfonyRequire = preg_replace('/\.x$/', '.x-dev', getenv('SYMFONY_REQUIRE') ?: ($composer->getPackage()->getExtra()['symfony']['require'] ?? '')); if ($composer2 = version_compare('2.0.0', PluginInterface::PLUGIN_API_VERSION, '<=')) { $rfs = Factory::createHttpDownloader($this->io, $this->config); $this->downloader = $downloader = new Downloader($composer, $io, $rfs); if ($symfonyRequire) { $this->filter = new PackageFilter($io, $symfonyRequire, $this->downloader); } $setRepositories = null; } else { $rfs = Factory::createRemoteFilesystem($this->io, $this->config); $this->rfs = $rfs = new ParallelDownloader($this->io, $this->config, $rfs->getOptions(), $rfs->isTlsDisabled()); $this->downloader = $downloader = new Downloader($composer, $io, $this->rfs); $rootPackage = $composer->getPackage(); $manager = RepositoryFactory::manager($this->io, $this->config, $composer->getEventDispatcher(), $this->rfs); $setRepositories = \Closure::bind(function (RepositoryManager $manager) use (&$symfonyRequire, $rootPackage, $downloader) { $manager->repositoryClasses = $this->repositoryClasses; $manager->setRepositoryClass('composer', TruncatedComposerRepository::class); $manager->repositories = $this->repositories; $i = 0; foreach (RepositoryFactory::defaultRepos(null, $this->config, $manager) as $repo) { $manager->repositories[$i++] = $repo; if ($repo instanceof TruncatedComposerRepository && $symfonyRequire) { $repo->setSymfonyRequire($symfonyRequire, $rootPackage, $downloader, $this->io); } } $manager->setLocalRepository($this->getLocalRepository()); }, $composer->getRepositoryManager(), RepositoryManager::class); $setRepositories($manager); $composer->setRepositoryManager($manager); } $composerFile = Factory::getComposerFile(); $composerLock = 'json' === pathinfo($composerFile, \PATHINFO_EXTENSION) ? substr($composerFile, 0, -4).'lock' : $composerFile.'.lock'; $symfonyLock = str_replace('composer', 'symfony', basename($composerLock)); $this->configurator = new Configurator($composer, $io, $this->options); $this->lock = new Lock(getenv('SYMFONY_LOCKFILE') ?: \dirname($composerLock).'/'.(basename($composerLock) !== $symfonyLock ? $symfonyLock : 'symfony.lock')); $disable = true; foreach (array_merge($composer->getPackage()->getRequires() ?? [], $composer->getPackage()->getDevRequires() ?? []) as $link) { // recipes apply only when symfony/flex is found in "require" or "require-dev" in the root package if ('symfony/flex' === $link->getTarget()) { $disable = false; break; } } if ($disable) { $downloader->disable(); } $populateRepoCacheDir = !$composer2 && __CLASS__ === self::class; if (!$composer2 && $composer->getPluginManager()) { foreach ($composer->getPluginManager()->getPlugins() as $plugin) { if (0 === strpos(\get_class($plugin), 'Hirak\Prestissimo\Plugin')) { if (method_exists($rfs, 'getRemoteContents')) { $plugin->disable(); } else { $this->cacheDirPopulated = true; } $populateRepoCacheDir = false; break; } } } $backtrace = $this->configureInstaller(); foreach ($backtrace as $trace) { if (!isset($trace['object']) || !isset($trace['args'][0])) { continue; } if (!$trace['object'] instanceof Application || !$trace['args'][0] instanceof ArgvInput) { continue; } // In Composer 1.0.*, $input knows about option and argument definitions // Since Composer >=1.1, $input contains only raw values $input = $trace['args'][0]; $app = $trace['object']; $resolver = new PackageResolver($this->downloader); if (version_compare('1.1.0', PluginInterface::PLUGIN_API_VERSION, '>')) { $note = $app->has('self-update') ? sprintf('`php %s self-update`', $_SERVER['argv'][0]) : 'https://getcomposer.org/'; $io->writeError('<warning>Some Symfony Flex features may not work as expected: your version of Composer is too old</>'); $io->writeError(sprintf('<warning>Please upgrade using %s</>', $note)); } try { $command = $input->getFirstArgument(); $command = $command ? $app->find($command)->getName() : null; } catch (\InvalidArgumentException $e) { } if ('create-project' === $command) { // detect Composer >=1.7 (using the Composer::VERSION constant doesn't work with snapshot builds) if (class_exists(Comparer::class)) { if ($input->hasOption('remove-vcs')) { $input->setOption('remove-vcs', true); } } else { $input->setInteractive(false); } $populateRepoCacheDir = $populateRepoCacheDir && !$input->hasOption('remove-vcs'); } elseif ('update' === $command) { $this->displayThanksReminder = 1; } elseif ('outdated' === $command) { $symfonyRequire = null; if ($setRepositories) { $setRepositories($manager); } } if (isset(self::$aliasResolveCommands[$command])) { if ($input->hasArgument('packages')) { $input->setArgument('packages', $resolver->resolve($input->getArgument('packages'), self::$aliasResolveCommands[$command])); } if (version_compare('2.0.0', PluginInterface::PLUGIN_API_VERSION, '>') && $input->hasOption('no-suggest')) { $input->setOption('no-suggest', true); } } if (!$composer2) { if ($input->hasParameterOption('--no-progress', true)) { $this->progress = false; } if ($input->hasParameterOption('--dry-run', true)) { $this->dryRun = true; } } if ($input->hasParameterOption('--prefer-lowest', true)) { // When prefer-lowest is set and no stable version has been released, // we consider "dev" more stable than "alpha", "beta" or "RC". This // allows testing lowest versions with potential fixes applied. BasePackage::$stabilities['dev'] = 1 + BasePackage::STABILITY_STABLE; } if ($populateRepoCacheDir && isset(self::$repoReadingCommands[$command]) && ('install' !== $command || (file_exists($composerFile = Factory::getComposerFile()) && !file_exists(substr($composerFile, 0, -4).'lock')))) { $this->populateRepoCacheDir(); } $app->add(new Command\UnpackCommand($resolver)); $app->add(new Command\RecipesCommand($this, $this->lock, $rfs)); $app->add(new Command\InstallRecipesCommand($this, $this->options->get('root-dir'), $this->options->get('runtime')['dotenv_path'] ?? '.env')); $app->add(new Command\UpdateRecipesCommand($this, $this->downloader, $rfs, $this->configurator, $this->options->get('root-dir'))); if (class_exists(Command\GenerateIdCommand::class)) { $app->add(new Command\GenerateIdCommand(null)); } $app->add(new Command\DumpEnvCommand($this->config, $this->options)); break; } } public function deactivate(Composer $composer, IOInterface $io) { self::$activated = false; } public function configureInstaller() { $backtrace = debug_backtrace(); foreach ($backtrace as $trace) { if (isset($trace['object']) && $trace['object'] instanceof Installer) { $this->installer = $trace['object']->setSuggestedPackagesReporter(new SuggestedPackagesReporter(new NullIO())); $updateAllowList = \Closure::bind(function () { return $this->updateWhitelist ?? $this->updateAllowList ?? null; }, $this->installer, $this->installer)(); if (['php' => 0] === $updateAllowList) { $this->dryRun = true; // prevent recipes from being uninstalled when removing a pack } } if (isset($trace['object']) && $trace['object'] instanceof GlobalCommand) { $this->downloader->disable(); } } return $backtrace; } public function configureProject(Event $event) { if (!$this->downloader->isEnabled()) { $this->io->writeError('<warning>Project configuration is disabled: "symfony/flex" not found in the root composer.json</>'); return; } // Remove LICENSE (which do not apply to the user project) @unlink('LICENSE'); // Update composer.json (project is proprietary by default) $file = Factory::getComposerFile(); $contents = file_get_contents($file); $manipulator = new JsonManipulator($contents); $json = JsonFile::parseJson($contents); // new projects are most of the time proprietary $manipulator->addMainKey('license', 'proprietary'); // extra.branch-alias doesn't apply to the project $manipulator->removeSubNode('extra', 'branch-alias'); // 'name' and 'description' are only required for public packages // don't use $manipulator->removeProperty() for BC with Composer 1.0 $contents = preg_replace(['{^\s*+"name":.*,$\n}m', '{^\s*+"description":.*,$\n}m'], '', $manipulator->getContents(), 1); file_put_contents($file, $contents); $this->updateComposerLock(); } public function recordFlexInstall(PackageEvent $event) { if (null === $this->reinstall && 'symfony/flex' === $event->getOperation()->getPackage()->getName()) { $this->reinstall = true; } } public function record(PackageEvent $event) { if ($this->shouldRecordOperation($event->getOperation(), $event->isDevMode(), $event->getComposer())) { $this->operations[] = $event->getOperation(); } } public function recordOperations(InstallerEvent $event) { if (!$event->isExecutingOperations()) { return; } $versionParser = new VersionParser(); $packages = []; foreach ($this->lock->all() as $name => $info) { $packages[] = new Package($name, $versionParser->normalize($info['version']), $info['version']); } $transation = \Closure::bind(function () use ($packages, $event) { return new Transaction($packages, $event->getTransaction()->resultPackageMap); }, null, Transaction::class)(); foreach ($transation->getOperations() as $operation) { if (!$operation instanceof UninstallOperation && $this->shouldRecordOperation($operation, $event->isDevMode(), $event->getComposer())) { $this->operations[] = $operation; } } } public function update(Event $event, $operations = []) { if ($operations) { $this->operations = $operations; } $this->install($event); $file = Factory::getComposerFile(); $contents = file_get_contents($file); $json = JsonFile::parseJson($contents); if (!$this->reinstall && !isset($json['flex-require']) && !isset($json['flex-require-dev'])) { $this->unpack($event); return; } // merge "flex-require" with "require" $manipulator = new JsonManipulator($contents); $sortPackages = $this->composer->getConfig()->get('sort-packages'); $symfonyVersion = $json['extra']['symfony']['require'] ?? null; $versions = $symfonyVersion ? $this->downloader->getVersions() : null; foreach (['require', 'require-dev'] as $type) { if (!isset($json['flex-'.$type])) { continue; } foreach ($json['flex-'.$type] as $package => $constraint) { if ($symfonyVersion && '*' === $constraint && isset($versions['splits'][$package])) { // replace unbounded constraints for symfony/* packages by extra.symfony.require $constraint = $symfonyVersion; } $manipulator->addLink($type, $package, $constraint, $sortPackages); } $manipulator->removeMainKey('flex-'.$type); } file_put_contents($file, $manipulator->getContents()); $this->reinstall($event, true); } public function install(Event $event) { $rootDir = $this->options->get('root-dir'); $runtime = $this->options->get('runtime'); $dotenvPath = $rootDir.'/'.($runtime['dotenv_path'] ?? '.env'); if (!file_exists($dotenvPath) && !file_exists($dotenvPath.'.local') && file_exists($dotenvPath.'.dist') && false === strpos(file_get_contents($dotenvPath.'.dist'), '.env.local')) { copy($dotenvPath.'.dist', $dotenvPath); } // Execute missing recipes $recipes = ScriptEvents::POST_UPDATE_CMD === $event->getName() ? $this->fetchRecipes($this->operations, $event instanceof UpdateEvent && $event->reset()) : []; $this->operations = []; // Reset the operation after getting recipes if (2 === $this->displayThanksReminder) { $love = '\\' === \DIRECTORY_SEPARATOR ? 'love' : '💖 '; $star = '\\' === \DIRECTORY_SEPARATOR ? 'star' : '★ '; $this->io->writeError(''); $this->io->writeError('What about running <comment>composer global require symfony/thanks && composer thanks</> now?'); $this->io->writeError(sprintf('This will spread some %s by sending a %s to the GitHub repositories of your fellow package maintainers.', $love, $star)); } $this->io->writeError(''); if (!$recipes) { if (ScriptEvents::POST_UPDATE_CMD === $event->getName()) { $this->finish($rootDir); } if ($this->downloader->isEnabled()) { $this->io->writeError('Run <comment>composer recipes</> at any time to see the status of your Symfony recipes.'); $this->io->writeError(''); } return; } $this->io->writeError(sprintf('<info>Symfony operations: %d recipe%s (%s)</>', \count($recipes), \count($recipes) > 1 ? 's' : '', $this->downloader->getSessionId())); $installContribs = $this->composer->getPackage()->getExtra()['symfony']['allow-contrib'] ?? false; $manifest = null; $originalComposerJsonHash = $this->getComposerJsonHash(); $postInstallRecipes = []; foreach ($recipes as $recipe) { if ('install' === $recipe->getJob() && !$installContribs && $recipe->isContrib()) { $warning = $this->io->isInteractive() ? 'WARNING' : 'IGNORING'; $this->io->writeError(sprintf(' - <warning> %s </> %s', $warning, $this->formatOrigin($recipe))); $question = sprintf(' The recipe for this package comes from the "contrib" repository, which is open to community contributions. Review the recipe at %s Do you want to execute this recipe? [<comment>y</>] Yes [<comment>n</>] No [<comment>a</>] Yes for all packages, only for the current installation session [<comment>p</>] Yes permanently, never ask again for this project (defaults to <comment>n</>): ', $recipe->getURL()); $answer = $this->io->askAndValidate( $question, function ($value) { if (null === $value) { return 'n'; } $value = strtolower($value[0]); if (!\in_array($value, ['y', 'n', 'a', 'p'])) { throw new \InvalidArgumentException('Invalid choice.'); } return $value; }, null, 'n' ); if ('n' === $answer) { continue; } if ('a' === $answer) { $installContribs = true; } if ('p' === $answer) { $installContribs = true; $json = new JsonFile(Factory::getComposerFile()); $manipulator = new JsonManipulator(file_get_contents($json->getPath())); $manipulator->addSubNode('extra', 'symfony.allow-contrib', true); file_put_contents($json->getPath(), $manipulator->getContents()); } } switch ($recipe->getJob()) { case 'install': $postInstallRecipes[] = $recipe; $this->io->writeError(sprintf(' - Configuring %s', $this->formatOrigin($recipe))); $this->configurator->install($recipe, $this->lock, [ 'force' => $event instanceof UpdateEvent && $event->force(), ]); $manifest = $recipe->getManifest(); if (isset($manifest['post-install-output'])) { $this->postInstallOutput[] = sprintf('<bg=yellow;fg=white> %s </> instructions:', $recipe->getName()); $this->postInstallOutput[] = ''; foreach ($manifest['post-install-output'] as $line) { $this->postInstallOutput[] = $this->options->expandTargetDir($line); } $this->postInstallOutput[] = ''; } break; case 'update': break; case 'uninstall': $this->io->writeError(sprintf(' - Unconfiguring %s', $this->formatOrigin($recipe))); $this->configurator->unconfigure($recipe, $this->lock); break; } } foreach ($postInstallRecipes as $recipe) { $this->configurator->postInstall($recipe, $this->lock, [ 'force' => $event instanceof UpdateEvent && $event->force(), ]); } if (null !== $manifest) { array_unshift( $this->postInstallOutput, '<bg=blue;fg=white> </>', '<bg=blue;fg=white> What\'s next? </>', '<bg=blue;fg=white> </>', '', '<info>Some files have been created and/or updated to configure your new packages.</>', 'Please <comment>review</>, <comment>edit</> and <comment>commit</> them: these files are <comment>yours</>.' ); } $this->finish($rootDir, $originalComposerJsonHash); } public function finish(string $rootDir, string $originalComposerJsonHash = null): void { $this->synchronizePackageJson($rootDir); $this->lock->write(); if ($originalComposerJsonHash && $this->getComposerJsonHash() !== $originalComposerJsonHash) { $this->updateComposerLock(); } } private function synchronizePackageJson(string $rootDir) { $rootDir = realpath($rootDir); $vendorDir = trim((new Filesystem())->makePathRelative($this->config->get('vendor-dir'), $rootDir), '/'); $executor = new ScriptExecutor($this->composer, $this->io, $this->options); $synchronizer = new PackageJsonSynchronizer($rootDir, $vendorDir, $executor); if ($synchronizer->shouldSynchronize()) { $lockData = $this->composer->getLocker()->getLockData(); if ($synchronizer->synchronize(array_merge($lockData['packages'] ?? [], $lockData['packages-dev'] ?? []))) { $this->io->writeError('<info>Synchronizing package.json with PHP packages</>'); $this->io->writeError('<warning>Don\'t forget to run npm install --force or yarn install --force to refresh your JavaScript dependencies!</>'); $this->io->writeError(''); } } } public function uninstall(Composer $composer, IOInterface $io) { $this->lock->delete(); } public function enableThanksReminder() { if (1 === $this->displayThanksReminder) { $this->displayThanksReminder = !class_exists(Thanks::class, false) && version_compare('1.1.0', PluginInterface::PLUGIN_API_VERSION, '<=') ? 2 : 0; } } public function executeAutoScripts(Event $event) { $event->stopPropagation(); // force reloading scripts as we might have added and removed during this run $json = new JsonFile(Factory::getComposerFile()); $jsonContents = $json->read(); $executor = new ScriptExecutor($this->composer, $this->io, $this->options); foreach ($jsonContents['scripts']['auto-scripts'] as $cmd => $type) { $executor->execute($type, $cmd); } $this->io->write($this->postInstallOutput); $this->postInstallOutput = []; } public function populateProvidersCacheDir(InstallerEvent $event) { $listed = []; $packages = []; $pool = $event->getPool(); $pool = \Closure::bind(function () { foreach ($this->providerRepos as $k => $repo) { $this->providerRepos[$k] = new class($repo) extends BaseComposerRepository { private $repo; public function __construct($repo) { $this->repo = $repo; } public function whatProvides(Pool $pool, $name, $bypassFilters = false) { $packages = []; foreach ($this->repo->whatProvides($pool, $name, $bypassFilters) as $k => $p) { $packages[$k] = clone $p; } return $packages; } }; } return $this; }, clone $pool, $pool)(); foreach ($event->getRequest()->getJobs() as $job) { if ('install' !== $job['cmd'] || false === strpos($job['packageName'], '/')) { continue; } $listed[$job['packageName']] = true; $packages[] = [$job['packageName'], $job['constraint']]; } $loadExtraRepos = !(new \ReflectionMethod(Pool::class, 'match'))->isPublic(); // Detect Composer < 1.7.3 $this->rfs->download($packages, function ($packageName, $constraint) use (&$listed, &$packages, $pool, $loadExtraRepos) { foreach ($pool->whatProvides($packageName, $constraint, true) as $package) { $links = $loadExtraRepos ? array_merge($package->getRequires(), $package->getConflicts(), $package->getReplaces()) : $package->getRequires(); foreach ($links as $link) { if (isset($listed[$link->getTarget()]) || false === strpos($link->getTarget(), '/')) { continue; } $listed[$link->getTarget()] = true; $packages[] = [$link->getTarget(), $link->getConstraint()]; } } }); } public function populateFilesCacheDir(InstallerEvent $event) { if ($this->cacheDirPopulated || $this->dryRun) { return; } $this->cacheDirPopulated = true; $downloads = []; $cacheDir = rtrim($this->config->get('cache-files-dir'), '\/').\DIRECTORY_SEPARATOR; $getCacheKey = function (PackageInterface $package, $processedUrl) { return $this->getCacheKey($package, $processedUrl); }; $getCacheKey = \Closure::bind($getCacheKey, new FileDownloader($this->io, $this->config), FileDownloader::class); foreach ($event->getOperations() as $op) { if ('install' === $op->getJobType()) { $package = $op->getPackage(); } elseif ('update' === $op->getJobType()) { $package = $op->getTargetPackage(); } else { continue; } if (!$fileUrl = $package->getDistUrl()) { continue; } if ($package->getDistMirrors()) { $fileUrl = current($package->getDistUrls()); } if (!preg_match('/^https?:/', $fileUrl) || !$originUrl = parse_url($fileUrl, \PHP_URL_HOST)) { continue; } if (file_exists($file = $cacheDir.$getCacheKey($package, $fileUrl))) { continue; } @mkdir(\dirname($file), 0775, true); if (!is_dir(\dirname($file))) { continue; } if (preg_match('#^https://github\.com/#', $package->getSourceUrl()) && preg_match('#^https://api\.github\.com/repos(/[^/]++/[^/]++/)zipball(.++)$#', $fileUrl, $m)) { $fileUrl = sprintf('https://codeload.github.com%slegacy.zip%s', $m[1], $m[2]); } $downloads[] = [$originUrl, $fileUrl, [], $file, false]; } if (1 < \count($downloads)) { $this->rfs->download($downloads, [$this->rfs, 'get'], false, $this->progress); } } public function onFileDownload(PreFileDownloadEvent $event) { if ($event->getRemoteFilesystem() !== $this->rfs) { $event->setRemoteFilesystem($this->rfs->setNextOptions($event->getRemoteFilesystem()->getOptions())); } } /** * @return Recipe[] */ public function fetchRecipes(array $operations, bool $reset): array { if (!$this->downloader->isEnabled()) { $this->io->writeError('<warning>Symfony recipes are disabled: "symfony/flex" not found in the root composer.json</>'); return []; } $devPackages = null; $data = $this->downloader->getRecipes($operations); $manifests = $data['manifests'] ?? []; $locks = $data['locks'] ?? []; // symfony/flex recipes should always be applied first $flexRecipe = []; // symfony/framework-bundle recipe should always be applied first after the metapackages $recipes = [ 'symfony/framework-bundle' => null, ]; $packRecipes = []; $metaRecipes = []; foreach ($operations as $operation) { if ($operation instanceof UpdateOperation) { $package = $operation->getTargetPackage(); } else { $package = $operation->getPackage(); } // FIXME: Multi name with getNames() $name = $package->getName(); $job = method_exists($operation, 'getOperationType') ? $operation->getOperationType() : $operation->getJobType(); if (!isset($manifests[$name]) && isset($data['conflicts'][$name])) { $this->io->writeError(sprintf(' - Skipping recipe for %s: all versions of the recipe conflict with your package versions.', $name)); continue; } while ($this->doesRecipeConflict($manifests[$name] ?? [], $operation)) { $this->downloader->removeRecipeFromIndex($name, $manifests[$name]['version']); $newData = $this->downloader->getRecipes([$operation]); $newManifests = $newData['manifests'] ?? []; if (!isset($newManifests[$name])) { // no older recipe found $this->io->writeError(sprintf(' - Skipping recipe for %s: all versions of the recipe conflict with your package versions.', $name)); continue 2; } // push the "old" recipe into the $manifests $manifests[$name] = $newManifests[$name]; $locks[$name] = $newData['locks'][$name]; } if ($operation instanceof InstallOperation && isset($locks[$name])) { $ref = $this->lock->get($name)['recipe']['ref'] ?? null; if (!$reset && $ref && ($locks[$name]['recipe']['ref'] ?? null) === $ref) { continue; } $this->lock->set($name, $locks[$name]); } elseif ($operation instanceof UninstallOperation) { if (!$this->lock->has($name)) { continue; } $this->lock->remove($name); } if (isset($manifests[$name])) { $recipe = new Recipe($package, $name, $job, $manifests[$name], $locks[$name] ?? []); if ('symfony-pack' === $package->getType()) { $packRecipes[$name] = $recipe; } elseif ('metapackage' === $package->getType()) { $metaRecipes[$name] = $recipe; } elseif ('symfony/flex' === $name) { $flexRecipe = [$name => $recipe]; } else { $recipes[$name] = $recipe; } } else { $bundles = []; if (null === $devPackages) { $devPackages = array_column($this->composer->getLocker()->getLockData()['packages-dev'], 'name'); } $envs = \in_array($name, $devPackages) ? ['dev', 'test'] : ['all']; $bundle = new SymfonyBundle($this->composer, $package, $job); foreach ($bundle->getClassNames() as $bundleClass) { $bundles[$bundleClass] = $envs; } if ($bundles) { $manifest = [ 'origin' => sprintf('%s:%s@auto-generated recipe', $name, $package->getPrettyVersion()), 'manifest' => ['bundles' => $bundles], ]; $recipes[$name] = new Recipe($package, $name, $job, $manifest); if ($operation instanceof InstallOperation) { $this->lock->set($name, ['version' => $package->getPrettyVersion()]); } } } } return array_merge($flexRecipe, $packRecipes, $metaRecipes, array_filter($recipes)); } public function truncatePackages(PrePoolCreateEvent $event) { if (!$this->filter) { return; } $rootPackage = $this->composer->getPackage(); $lockedPackages = $event->getRequest()->getFixedOrLockedPackages(); $event->setPackages($this->filter->removeLegacyPackages($event->getPackages(), $rootPackage, $lockedPackages)); } public function getComposerJsonHash(): string { return md5_file(Factory::getComposerFile()); } public function getLock(): Lock { if (null === $this->lock) { throw new \Exception('Cannot access lock before calling activate().'); } return $this->lock; } private function initOptions(): Options { $extra = $this->composer->getPackage()->getExtra(); $options = array_merge([ 'bin-dir' => 'bin', 'conf-dir' => 'conf', 'config-dir' => 'config', 'src-dir' => 'src', 'var-dir' => 'var', 'public-dir' => 'public', 'root-dir' => $extra['symfony']['root-dir'] ?? '.', 'runtime' => $extra['runtime'] ?? [], ], $extra); return new Options($options, $this->io); } private function formatOrigin(Recipe $recipe): string { if (method_exists($recipe, 'getFormattedOrigin')) { return $recipe->getFormattedOrigin(); } // BC with upgrading from flex < 1.18 $origin = $recipe->getOrigin(); // symfony/translation:3.3@github.com/symfony/recipes:branch if (!preg_match('/^([^:]++):([^@]++)@(.+)$/', $origin, $matches)) { return $origin; } return sprintf('<info>%s</> (<comment>>=%s</>): From %s', $matches[1], $matches[2], 'auto-generated recipe' === $matches[3] ? '<comment>'.$matches[3].'</>' : $matches[3]); } private function shouldRecordOperation(OperationInterface $operation, bool $isDevMode, Composer $composer = null): bool { if ($this->dryRun || $this->reinstall) { return false; } if ($operation instanceof UpdateOperation) { $package = $operation->getTargetPackage(); } else { $package = $operation->getPackage(); } // when Composer runs with --no-dev, ignore uninstall operations on packages from require-dev if (!$isDevMode && $operation instanceof UninstallOperation) { foreach (($composer ?? $this->composer)->getLocker()->getLockData()['packages-dev'] as $p) { if ($package->getName() === $p['name']) { return false; } } } // FIXME: Multi name with getNames() $name = $package->getName(); if ($operation instanceof InstallOperation) { if (!$this->lock->has($name)) { return true; } } elseif ($operation instanceof UninstallOperation) { return true; } return false; } private function populateRepoCacheDir() { $repos = []; foreach ($this->composer->getPackage()->getRepositories() as $name => $repo) { if (!isset($repo['type']) || 'composer' !== $repo['type'] || !empty($repo['force-lazy-providers'])) { continue; } if (!preg_match('#^http(s\??)?://#', $repo['url'])) { continue; } $repo = new ComposerRepository($repo, $this->io, $this->config, null, $this->rfs); $repos[] = [$repo]; } $this->rfs->download($repos, function ($repo) { ParallelDownloader::$cacheNext = true; $repo->getProviderNames(); }); } private function updateComposerLock() { $lock = substr(Factory::getComposerFile(), 0, -4).'lock'; $composerJson = file_get_contents(Factory::getComposerFile()); $lockFile = new JsonFile($lock, null, $this->io); if (version_compare('2.0.0', PluginInterface::PLUGIN_API_VERSION, '>')) { $locker = new Locker($this->io, $lockFile, $this->composer->getRepositoryManager(), $this->composer->getInstallationManager(), $composerJson); } else { $locker = new Locker($this->io, $lockFile, $this->composer->getInstallationManager(), $composerJson); } $lockData = $locker->getLockData(); $lockData['content-hash'] = Locker::getContentHash($composerJson); $lockFile->write($lockData); } private function unpack(Event $event) { $jsonPath = Factory::getComposerFile(); $json = JsonFile::parseJson(file_get_contents($jsonPath)); $sortPackages = $this->composer->getConfig()->get('sort-packages'); $unpackOp = new Operation(true, $sortPackages); foreach (['require', 'require-dev'] as $type) { foreach ($json[$type] ?? [] as $package => $constraint) { $unpackOp->addPackage($package, $constraint, 'require-dev' === $type); } } $unpacker = new Unpacker($this->composer, new PackageResolver($this->downloader), $this->dryRun); $result = $unpacker->unpack($unpackOp); if (!$result->getUnpacked()) { return; } $this->io->writeError('<info>Unpacking Symfony packs</>'); foreach ($result->getUnpacked() as $pkg) { $this->io->writeError(sprintf(' - Unpacked <info>%s</>', $pkg->getName())); } $unpacker->updateLock($result, $this->io); $this->reinstall($event, false); } private function reinstall(Event $event, bool $update) { $this->reinstall = false; $event->stopPropagation(); $ed = $this->composer->getEventDispatcher(); $disableScripts = !method_exists($ed, 'setRunScripts') || !((array) $ed)["\0*\0runScripts"]; $composer = Factory::create($this->io, null, false, $disableScripts); $installer = clone $this->installer; $installer->__construct( $this->io, $composer->getConfig(), $composer->getPackage(), $composer->getDownloadManager(), $composer->getRepositoryManager(), $composer->getLocker(), $composer->getInstallationManager(), $composer->getEventDispatcher(), $composer->getAutoloadGenerator() ); if (!$update && method_exists($installer, 'setUpdateAllowList')) { $installer->setUpdateAllowList(['php']); } elseif (!$update) { $installer->setUpdateWhiteList(['php']); } if (method_exists($installer, 'setSkipSuggest')) { $installer->setSkipSuggest(true); } $installer->run(); $this->io->write($this->postInstallOutput); $this->postInstallOutput = []; } public static function getSubscribedEvents(): array { if (!self::$activated) { return []; } $events = [ PackageEvents::POST_PACKAGE_UNINSTALL => 'record', ScriptEvents::POST_CREATE_PROJECT_CMD => 'configureProject', ScriptEvents::POST_INSTALL_CMD => 'install', ScriptEvents::PRE_UPDATE_CMD => 'configureInstaller', ScriptEvents::POST_UPDATE_CMD => 'update', 'auto-scripts' => 'executeAutoScripts', ]; if (version_compare('2.0.0', PluginInterface::PLUGIN_API_VERSION, '>')) { $events += [ PackageEvents::POST_PACKAGE_INSTALL => [['recordFlexInstall'], ['record']], PackageEvents::POST_PACKAGE_UPDATE => [['record'], ['enableThanksReminder']], InstallerEvents::PRE_DEPENDENCIES_SOLVING => [['populateProvidersCacheDir', \PHP_INT_MAX]], InstallerEvents::POST_DEPENDENCIES_SOLVING => [['populateFilesCacheDir', \PHP_INT_MAX]], PackageEvents::PRE_PACKAGE_INSTALL => [['populateFilesCacheDir', ~\PHP_INT_MAX]], PackageEvents::PRE_PACKAGE_UPDATE => [['populateFilesCacheDir', ~\PHP_INT_MAX]], PluginEvents::PRE_FILE_DOWNLOAD => 'onFileDownload', ]; } else { $events += [ PackageEvents::POST_PACKAGE_UPDATE => 'enableThanksReminder', PackageEvents::POST_PACKAGE_INSTALL => 'recordFlexInstall', InstallerEvents::PRE_OPERATIONS_EXEC => 'recordOperations', PluginEvents::PRE_POOL_CREATE => 'truncatePackages', ]; } return $events; } private function doesRecipeConflict(array $recipeData, OperationInterface $operation): bool { if (empty($recipeData['manifest']['conflict']) || $operation instanceof UninstallOperation) { return false; } $lockedRepository = $this->composer->getLocker()->getLockedRepository(); foreach ($recipeData['manifest']['conflict'] as $conflictingPackage => $constraint) { if ($lockedRepository->findPackage($conflictingPackage, $constraint)) { return true; } } return false; } } src/GithubApi.php 0000644 00000014055 15120140521 0007714 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\Util\HttpDownloader; use Composer\Util\RemoteFilesystem; class GithubApi { /** @var HttpDownloader|RemoteFilesystem */ private $downloader; public function __construct($downloader) { $this->downloader = $downloader; } /** * Attempts to find data about when the recipe was installed. * * Returns an array containing: * commit: The git sha of the last commit of the recipe * date: The date of the commit * new_commits: An array of commit sha's in this recipe's directory+version since the commit * The key is the sha & the value is the date */ public function findRecipeCommitDataFromTreeRef(string $package, string $repo, string $branch, string $version, string $lockRef): ?array { $repositoryName = $this->getRepositoryName($repo); if (!$repositoryName) { return null; } $recipePath = sprintf('%s/%s', $package, $version); $commitsData = $this->requestGitHubApi(sprintf( 'https://api.github.com/repos/%s/commits?path=%s&sha=%s', $repositoryName, $recipePath, $branch )); $commitShas = []; foreach ($commitsData as $commitData) { $commitShas[$commitData['sha']] = $commitData['commit']['committer']['date']; // go back the commits one-by-one $treeUrl = $commitData['commit']['tree']['url'].'?recursive=true'; // fetch the full tree, then look for the tree for the package path $treeData = $this->requestGitHubApi($treeUrl); foreach ($treeData['tree'] as $treeItem) { if ($treeItem['path'] !== $recipePath) { continue; } if ($treeItem['sha'] === $lockRef) { // remove *this* commit from the new commits list array_pop($commitShas); return [ // shorten for brevity 'commit' => substr($commitData['sha'], 0, 7), 'date' => $commitData['commit']['committer']['date'], 'new_commits' => $commitShas, ]; } } } return null; } public function getVersionsOfRecipe(string $repo, string $branch, string $recipePath): ?array { $repositoryName = $this->getRepositoryName($repo); if (!$repositoryName) { return null; } $url = sprintf( 'https://api.github.com/repos/%s/contents/%s?ref=%s', $repositoryName, $recipePath, $branch ); $contents = $this->requestGitHubApi($url); $versions = []; foreach ($contents as $fileData) { if ('dir' !== $fileData['type']) { continue; } $versions[] = $fileData['name']; } return $versions; } public function getCommitDataForPath(string $repo, string $path, string $branch): array { $repositoryName = $this->getRepositoryName($repo); if (!$repositoryName) { return []; } $commitsData = $this->requestGitHubApi(sprintf( 'https://api.github.com/repos/%s/commits?path=%s&sha=%s', $repositoryName, $path, $branch )); $data = []; foreach ($commitsData as $commitData) { $data[$commitData['sha']] = $commitData['commit']['committer']['date']; } return $data; } public function getPullRequestForCommit(string $commit, string $repo): ?array { $data = $this->requestGitHubApi('https://api.github.com/search/issues?q='.$commit); if (0 === \count($data['items'])) { return null; } $repositoryName = $this->getRepositoryName($repo); if (!$repositoryName) { return null; } $bestItem = null; foreach ($data['items'] as $item) { // make sure the PR referenced isn't from a different repository if (false === strpos($item['html_url'], sprintf('%s/pull', $repositoryName))) { continue; } if (null === $bestItem) { $bestItem = $item; continue; } // find the first PR to reference - avoids rare cases where an invalid // PR that references *many* commits is first // e.g. https://api.github.com/search/issues?q=a1a70353f64f405cfbacfc4ce860af623442d6e5 if ($item['number'] < $bestItem['number']) { $bestItem = $item; } } if (!$bestItem) { return null; } return [ 'number' => $bestItem['number'], 'url' => $bestItem['html_url'], 'title' => $bestItem['title'], ]; } private function requestGitHubApi(string $path) { if ($this->downloader instanceof HttpDownloader) { $contents = $this->downloader->get($path)->getBody(); } else { $contents = $this->downloader->getContents('api.github.com', $path, false); } return json_decode($contents, true); } /** * Converts the "repo" stored in symfony.lock to a repository name. * * For example: "github.com/symfony/recipes" => "symfony/recipes" */ private function getRepositoryName(string $repo): ?string { // only supports public repository placement if (0 !== strpos($repo, 'github.com')) { return null; } $parts = explode('/', $repo); if (3 !== \count($parts)) { return null; } return implode('/', [$parts[1], $parts[2]]); } } src/InformationOperation.php 0000644 00000003562 15120140521 0012207 0 ustar 00 <?php namespace Symfony\Flex; use Composer\DependencyResolver\Operation\OperationInterface; use Composer\Package\PackageInterface; /** * @author Maxime Hélias <maximehelias16@gmail.com> */ class InformationOperation implements OperationInterface { private $package; private $recipeRef = null; private $version = null; public function __construct(PackageInterface $package) { $this->package = $package; } /** * Call to get information about a specific version of a recipe. * * Both $recipeRef and $version would normally come from the symfony.lock file. */ public function setSpecificRecipeVersion(string $recipeRef, string $version) { $this->recipeRef = $recipeRef; $this->version = $version; } /** * Returns package instance. * * @return PackageInterface */ public function getPackage() { return $this->package; } public function getRecipeRef(): ?string { return $this->recipeRef; } public function getVersion(): ?string { return $this->version; } public function getJobType() { return 'information'; } /** * {@inheritdoc} */ public function getOperationType() { return 'information'; } /** * {@inheritdoc} */ public function show($lock) { $pretty = method_exists($this->package, 'getFullPrettyVersion') ? $this->package->getFullPrettyVersion() : $this->formatVersion($this->package); return 'Information '.$this->package->getPrettyName().' ('.$pretty.')'; } /** * {@inheritdoc} */ public function __toString() { return $this->show(false); } /** * Compatibility for Composer 1.x, not needed in Composer 2. */ public function getReason() { return null; } } src/Lock.php 0000644 00000003527 15120140521 0006732 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\Json\JsonFile; /** * @author Fabien Potencier <fabien@symfony.com> */ class Lock { private $json; private $lock = []; private $changed = false; public function __construct($lockFile) { $this->json = new JsonFile($lockFile); if ($this->json->exists()) { $this->lock = $this->json->read(); } } public function has($name): bool { return \array_key_exists($name, $this->lock); } public function add($name, $data) { $current = $this->lock[$name] ?? []; $this->lock[$name] = array_merge($current, $data); $this->changed = true; } public function get($name) { return $this->lock[$name] ?? null; } public function set($name, $data) { if (!\array_key_exists($name, $this->lock) || $data !== $this->lock[$name]) { $this->lock[$name] = $data; $this->changed = true; } } public function remove($name) { if (\array_key_exists($name, $this->lock)) { unset($this->lock[$name]); $this->changed = true; } } public function write() { if (!$this->changed) { return; } if ($this->lock) { ksort($this->lock); $this->json->write($this->lock); } elseif ($this->json->exists()) { @unlink($this->json->getPath()); } } public function delete() { @unlink($this->json->getPath()); } public function all(): array { return $this->lock; } } src/Options.php 0000644 00000004452 15120140521 0007473 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\IO\IOInterface; use Composer\Util\ProcessExecutor; /** * @author Fabien Potencier <fabien@symfony.com> */ class Options { private $options; private $writtenFiles = []; private $io; public function __construct(array $options = [], IOInterface $io = null) { $this->options = $options; $this->io = $io; } public function get(string $name) { return $this->options[$name] ?? null; } public function expandTargetDir(string $target): string { return preg_replace_callback('{%(.+?)%}', function ($matches) { $option = str_replace('_', '-', strtolower($matches[1])); if (!isset($this->options[$option])) { return $matches[0]; } return rtrim($this->options[$option], '/'); }, $target); } public function shouldWriteFile(string $file, bool $overwrite): bool { if (isset($this->writtenFiles[$file])) { return false; } $this->writtenFiles[$file] = true; if (!file_exists($file)) { return true; } if (!$overwrite) { return false; } if (!filesize($file)) { return true; } exec('git status --short --ignored --untracked-files=all -- '.ProcessExecutor::escape($file).' 2>&1', $output, $status); if (0 !== $status) { return $this->io && $this->io->askConfirmation(sprintf('Cannot determine the state of the "%s" file, overwrite anyway? [y/N] ', $file), false); } if (empty($output[0]) || preg_match('/^[ AMDRCU][ D][ \t]/', $output[0])) { return true; } $name = basename($file); $name = \strlen($output[0]) - \strlen($name) === strrpos($output[0], $name) ? substr($output[0], 3) : $name; return $this->io && $this->io->askConfirmation(sprintf('File "%s" has uncommitted changes, overwrite? [y/N] ', $name), false); } public function toArray(): array { return $this->options; } } src/PackageFilter.php 0000644 00000012176 15120140521 0010543 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\IO\IOInterface; use Composer\Package\AliasPackage; use Composer\Package\PackageInterface; use Composer\Package\RootPackageInterface; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Intervals; use Composer\Semver\VersionParser; /** * @author Nicolas Grekas <p@tchwork.com> */ class PackageFilter { private $versions; private $versionParser; private $symfonyRequire; private $symfonyConstraints; private $downloader; private $io; public function __construct(IOInterface $io, string $symfonyRequire, Downloader $downloader) { $this->versionParser = new VersionParser(); $this->symfonyRequire = $symfonyRequire; $this->symfonyConstraints = $this->versionParser->parseConstraints($symfonyRequire); $this->downloader = $downloader; $this->io = $io; } /** * @param PackageInterface[] $data * @param PackageInterface[] $lockedPackages * * @return PackageInterface[] */ public function removeLegacyPackages(array $data, RootPackageInterface $rootPackage, array $lockedPackages): array { if (!$this->symfonyConstraints || !$data) { return $data; } $lockedVersions = []; foreach ($lockedPackages as $package) { $lockedVersions[$package->getName()] = [$package->getVersion()]; if ($package instanceof AliasPackage) { $lockedVersions[$package->getName()][] = $package->getAliasOf()->getVersion(); } } $rootConstraints = []; foreach ($rootPackage->getRequires() + $rootPackage->getDevRequires() as $name => $link) { $rootConstraints[$name] = $link->getConstraint(); } $knownVersions = $this->getVersions(); $filteredPackages = []; $symfonyPackages = []; $oneSymfony = false; foreach ($data as $package) { $name = $package->getName(); $versions = [$package->getVersion()]; if ($package instanceof AliasPackage) { $versions[] = $package->getAliasOf()->getVersion(); } if ('symfony/symfony' !== $name && ( !isset($knownVersions['splits'][$name]) || array_intersect($versions, $lockedVersions[$name] ?? []) || (isset($rootConstraints[$name]) && !Intervals::haveIntersections($this->symfonyConstraints, $rootConstraints[$name])) )) { $filteredPackages[] = $package; continue; } if (null !== $alias = $package->getExtra()['branch-alias'][$package->getVersion()] ?? null) { $versions[] = $this->versionParser->normalize($alias); } foreach ($versions as $version) { if ($this->symfonyConstraints->matches(new Constraint('==', $version))) { $filteredPackages[] = $package; $oneSymfony = $oneSymfony || 'symfony/symfony' === $name; continue 2; } } if ('symfony/symfony' === $name) { $symfonyPackages[] = $package; } elseif (null !== $this->io) { $this->io->writeError(sprintf('<info>Restricting packages listed in "symfony/symfony" to "%s"</>', $this->symfonyRequire)); $this->io = null; } } if ($symfonyPackages && !$oneSymfony) { $filteredPackages = array_merge($filteredPackages, $symfonyPackages); } return $filteredPackages; } private function getVersions(): array { if (null !== $this->versions) { return $this->versions; } $versions = $this->downloader->getVersions(); $this->downloader = null; $okVersions = []; if (!isset($versions['splits'])) { throw new \LogicException('The Flex index is missing a "splits" entry. Did you forget to add "flex://defaults" in the "extra.symfony.endpoint" array of your composer.json?'); } foreach ($versions['splits'] as $name => $vers) { foreach ($vers as $i => $v) { if (!isset($okVersions[$v])) { $okVersions[$v] = false; $w = '.x' === substr($v, -2) ? $versions['next'] : $v; for ($j = 0; $j < 60; ++$j) { if ($this->symfonyConstraints->matches(new Constraint('==', $w.'.'.$j.'.0'))) { $okVersions[$v] = true; break; } } } if (!$okVersions[$v]) { unset($vers[$i]); } } if (!$vers || $vers === $versions['splits'][$name]) { unset($versions['splits'][$name]); } } return $this->versions = $versions; } } src/PackageJsonSynchronizer.php 0000644 00000033526 15120140521 0012647 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; use Composer\Semver\VersionParser; use Seld\JsonLint\ParsingException; /** * Synchronize package.json files detected in installed PHP packages with * the current application. */ class PackageJsonSynchronizer { private $rootDir; private $vendorDir; private $scriptExecutor; private $versionParser; public function __construct(string $rootDir, string $vendorDir, ScriptExecutor $scriptExecutor) { $this->rootDir = $rootDir; $this->vendorDir = $vendorDir; $this->scriptExecutor = $scriptExecutor; $this->versionParser = new VersionParser(); } public function shouldSynchronize(): bool { return $this->rootDir && (file_exists($this->rootDir.'/package.json') || file_exists($this->rootDir.'/importmap.php')); } public function synchronize(array $phpPackages): bool { if (file_exists($this->rootDir.'/importmap.php')) { $this->synchronizeForAssetMapper($phpPackages); return false; } try { JsonFile::parseJson(file_get_contents($this->rootDir.'/package.json')); } catch (ParsingException $e) { // if package.json is invalid (possible during a recipe upgrade), we can't update the file return false; } $didChangePackageJson = $this->removeObsoletePackageJsonLinks(); $dependencies = []; $phpPackages = $this->normalizePhpPackages($phpPackages); foreach ($phpPackages as $phpPackage) { foreach ($this->resolvePackageJsonDependencies($phpPackage) as $dependency => $constraint) { $dependencies[$dependency][$phpPackage['name']] = $constraint; } } $didChangePackageJson = $this->registerDependenciesInPackageJson($dependencies) || $didChangePackageJson; // Register controllers and entrypoints in controllers.json $this->updateControllersJsonFile($phpPackages); return $didChangePackageJson; } private function synchronizeForAssetMapper(array $phpPackages): void { $importMapEntries = []; $phpPackages = $this->normalizePhpPackages($phpPackages); foreach ($phpPackages as $phpPackage) { foreach ($this->resolveImportMapPackages($phpPackage) as $name => $dependencyConfig) { $importMapEntries[$name] = $dependencyConfig; } } $this->updateImportMap($importMapEntries); $this->updateControllersJsonFile($phpPackages); } private function removeObsoletePackageJsonLinks(): bool { $didChangePackageJson = false; $manipulator = new JsonManipulator(file_get_contents($this->rootDir.'/package.json')); $content = json_decode($manipulator->getContents(), true); $jsDependencies = $content['dependencies'] ?? []; $jsDevDependencies = $content['devDependencies'] ?? []; foreach (['dependencies' => $jsDependencies, 'devDependencies' => $jsDevDependencies] as $key => $packages) { foreach ($packages as $name => $version) { if ('@' !== $name[0] || 0 !== strpos($version, 'file:'.$this->vendorDir.'/') || false === strpos($version, '/assets')) { continue; } if (file_exists($this->rootDir.'/'.substr($version, 5).'/package.json')) { continue; } $manipulator->removeSubNode($key, $name); $didChangePackageJson = true; } } file_put_contents($this->rootDir.'/package.json', $manipulator->getContents()); return $didChangePackageJson; } private function resolvePackageJsonDependencies($phpPackage): array { $dependencies = []; if (!$packageJson = $this->resolvePackageJson($phpPackage)) { return $dependencies; } if ($packageJson->read()['symfony']['needsPackageAsADependency'] ?? true) { $dependencies['@'.$phpPackage['name']] = 'file:'.substr($packageJson->getPath(), 1 + \strlen($this->rootDir), -13); } foreach ($packageJson->read()['peerDependencies'] ?? [] as $peerDependency => $constraint) { $dependencies[$peerDependency] = $constraint; } return $dependencies; } private function resolveImportMapPackages($phpPackage): array { if (!$packageJson = $this->resolvePackageJson($phpPackage)) { return []; } $dependencies = []; foreach ($packageJson->read()['symfony']['importmap'] ?? [] as $importMapName => $constraintConfig) { if (\is_array($constraintConfig)) { $constraint = $constraintConfig['version'] ?? []; $preload = $constraintConfig['preload'] ?? false; $package = $constraintConfig['package'] ?? $importMapName; } else { $constraint = $constraintConfig; $preload = false; $package = $importMapName; } if (0 === strpos($constraint, 'path:')) { $path = substr($constraint, 5); $path = str_replace('%PACKAGE%', \dirname($packageJson->getPath()), $path); $dependencies[$importMapName] = [ 'path' => $path, 'preload' => $preload, ]; continue; } $dependencies[$importMapName] = [ 'version' => $constraint, 'package' => $package, 'preload' => $preload, ]; } return $dependencies; } private function registerDependenciesInPackageJson(array $flexDependencies): bool { $didChangePackageJson = false; $manipulator = new JsonManipulator(file_get_contents($this->rootDir.'/package.json')); $content = json_decode($manipulator->getContents(), true); foreach ($flexDependencies as $dependency => $constraints) { if (1 !== \count($constraints) && 1 !== \count(array_count_values($constraints))) { // If the flex packages have a colliding peer dependency, leave the resolution to the user continue; } $constraint = array_shift($constraints); $parentNode = isset($content['dependencies'][$dependency]) ? 'dependencies' : 'devDependencies'; if (!isset($content[$parentNode][$dependency])) { $content['devDependencies'][$dependency] = $constraint; $didChangePackageJson = true; } elseif ($constraint !== $content[$parentNode][$dependency]) { if ($this->shouldUpdateConstraint($content[$parentNode][$dependency], $constraint)) { $content[$parentNode][$dependency] = $constraint; $didChangePackageJson = true; } } } if ($didChangePackageJson) { if (isset($content['dependencies'])) { $manipulator->addMainKey('dependencies', $content['dependencies']); } if (isset($content['devDependencies'])) { $devDependencies = $content['devDependencies']; uksort($devDependencies, 'strnatcmp'); $manipulator->addMainKey('devDependencies', $devDependencies); } $newContents = $manipulator->getContents(); if ($newContents === file_get_contents($this->rootDir.'/package.json')) { return false; } file_put_contents($this->rootDir.'/package.json', $manipulator->getContents()); } return $didChangePackageJson; } private function shouldUpdateConstraint(string $existingConstraint, string $constraint) { try { $existingConstraint = $this->versionParser->parseConstraints($existingConstraint); $constraint = $this->versionParser->parseConstraints($constraint); return !$existingConstraint->matches($constraint); } catch (\UnexpectedValueException $e) { return true; } } /** * @param array<string, array{path?: string, preload: bool, package?: string, version?: string}> $importMapEntries */ private function updateImportMap(array $importMapEntries): void { if (!$importMapEntries) { return; } $importMapData = include $this->rootDir.'/importmap.php'; foreach ($importMapEntries as $name => $importMapEntry) { if (isset($importMapData[$name])) { continue; } if (isset($importMapEntry['path'])) { $arguments = [$name, '--path='.$importMapEntry['path']]; if ($importMapEntry['preload']) { $arguments[] = '--preload'; } $this->scriptExecutor->execute( 'symfony-cmd', 'importmap:require', $arguments ); continue; } if (isset($importMapEntry['version'])) { $packageName = $importMapEntry['package'].'@'.$importMapEntry['version']; if ($importMapEntry['package'] !== $name) { $packageName .= '='.$name; } $arguments = [$packageName]; if ($importMapEntry['preload']) { $arguments[] = '--preload'; } $this->scriptExecutor->execute( 'symfony-cmd', 'importmap:require', $arguments ); continue; } throw new \InvalidArgumentException(sprintf('Invalid importmap entry: "%s".', var_export($importMapEntry, true))); } } private function updateControllersJsonFile(array $phpPackages) { if (!file_exists($controllersJsonPath = $this->rootDir.'/assets/controllers.json')) { return; } $previousControllersJson = (new JsonFile($controllersJsonPath))->read(); $newControllersJson = [ 'controllers' => [], 'entrypoints' => $previousControllersJson['entrypoints'], ]; foreach ($phpPackages as $phpPackage) { if (!$packageJson = $this->resolvePackageJson($phpPackage)) { continue; } $name = '@'.$phpPackage['name']; foreach ($packageJson->read()['symfony']['controllers'] ?? [] as $controllerName => $defaultConfig) { // If the package has just been added (no config), add the default config provided by the package if (!isset($previousControllersJson['controllers'][$name][$controllerName])) { $config = []; $config['enabled'] = $defaultConfig['enabled']; $config['fetch'] = $defaultConfig['fetch'] ?? 'eager'; if (isset($defaultConfig['autoimport'])) { $config['autoimport'] = $defaultConfig['autoimport']; } $newControllersJson['controllers'][$name][$controllerName] = $config; continue; } // Otherwise, the package exists: merge new config with user config $previousConfig = $previousControllersJson['controllers'][$name][$controllerName]; $config = []; $config['enabled'] = $previousConfig['enabled']; $config['fetch'] = $previousConfig['fetch'] ?? 'eager'; if (isset($defaultConfig['autoimport'])) { $config['autoimport'] = []; // Use for each autoimport either the previous config if one existed or the default config otherwise foreach ($defaultConfig['autoimport'] as $autoimport => $enabled) { $config['autoimport'][$autoimport] = $previousConfig['autoimport'][$autoimport] ?? $enabled; } } $newControllersJson['controllers'][$name][$controllerName] = $config; } foreach ($packageJson->read()['symfony']['entrypoints'] ?? [] as $entrypoint => $filename) { if (!isset($newControllersJson['entrypoints'][$entrypoint])) { $newControllersJson['entrypoints'][$entrypoint] = $filename; } } } file_put_contents($controllersJsonPath, json_encode($newControllersJson, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)."\n"); } private function resolvePackageJson(array $phpPackage): ?JsonFile { $packageDir = $this->rootDir.'/'.$this->vendorDir.'/'.$phpPackage['name']; if (!\in_array('symfony-ux', $phpPackage['keywords'] ?? [], true)) { return null; } foreach (['/assets', '/Resources/assets', '/src/Resources/assets'] as $subdir) { $packageJsonPath = $packageDir.$subdir.'/package.json'; if (!file_exists($packageJsonPath)) { continue; } return new JsonFile($packageJsonPath); } return null; } private function normalizePhpPackages(array $phpPackages): array { foreach ($phpPackages as $k => $phpPackage) { if (\is_string($phpPackage)) { // support for smooth upgrades from older flex versions $phpPackages[$k] = $phpPackage = [ 'name' => $phpPackage, 'keywords' => ['symfony-ux'], ]; } } return $phpPackages; } } src/PackageResolver.php 0000644 00000012512 15120140521 0011111 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\Factory; use Composer\Package\Version\VersionParser; use Composer\Repository\PlatformRepository; /** * @author Fabien Potencier <fabien@symfony.com> */ class PackageResolver { private static $SYMFONY_VERSIONS = ['lts', 'previous', 'stable', 'next', 'dev']; private $downloader; public function __construct(Downloader $downloader) { $this->downloader = $downloader; } public function resolve(array $arguments = [], bool $isRequire = false): array { // first pass split on : and = to resolve package names $packages = []; foreach ($arguments as $i => $argument) { if ((false !== $pos = strpos($argument, ':')) || (false !== $pos = strpos($argument, '='))) { $package = $this->resolvePackageName(substr($argument, 0, $pos), $i, $isRequire); $version = substr($argument, $pos + 1); $packages[] = $package.':'.$version; } else { $packages[] = $this->resolvePackageName($argument, $i, $isRequire); } } // second pass to resolve versions $versionParser = new VersionParser(); $requires = []; foreach ($versionParser->parseNameVersionPairs($packages) as $package) { $requires[] = $package['name'].$this->parseVersion($package['name'], $package['version'] ?? '', $isRequire); } return array_unique($requires); } public function parseVersion(string $package, string $version, bool $isRequire): string { if (0 !== strpos($package, 'symfony/')) { return $version ? ':'.$version : ''; } $versions = $this->downloader->getVersions(); if (!isset($versions['splits'][$package])) { return $version ? ':'.$version : ''; } if (!$version || '*' === $version) { try { $config = @json_decode(file_get_contents(Factory::getComposerFile()), true); } finally { if (!$isRequire || !(isset($config['extra']['symfony']['require']) || isset($config['require']['symfony/framework-bundle']))) { return ''; } } $version = $config['extra']['symfony']['require'] ?? $config['require']['symfony/framework-bundle']; } elseif ('dev' === $version) { $version = '^'.$versions['dev-name'].'@dev'; } elseif ('next' === $version) { $version = '^'.$versions[$version].'@dev'; } elseif (\in_array($version, self::$SYMFONY_VERSIONS, true)) { $version = '^'.$versions[$version]; } return ':'.$version; } private function resolvePackageName(string $argument, int $position, bool $isRequire): string { $skippedPackages = ['mirrors', 'nothing', '']; if (!$isRequire) { $skippedPackages[] = 'lock'; } if (false !== strpos($argument, '/') || preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $argument) || preg_match('{(?<=[a-z0-9_/-])\*|\*(?=[a-z0-9_/-])}i', $argument) || \in_array($argument, $skippedPackages)) { return $argument; } $aliases = $this->downloader->getAliases(); if (isset($aliases[$argument])) { $argument = $aliases[$argument]; } else { // is it a version or an alias that does not exist? try { $versionParser = new VersionParser(); $versionParser->parseConstraints($argument); } catch (\UnexpectedValueException $e) { // is it a special Symfony version? if (!\in_array($argument, self::$SYMFONY_VERSIONS, true)) { $this->throwAlternatives($argument, $position); } } } return $argument; } /** * @throws \UnexpectedValueException */ private function throwAlternatives(string $argument, int $position) { $alternatives = []; foreach ($this->downloader->getAliases() as $alias => $package) { $lev = levenshtein($argument, $alias); if ($lev <= \strlen($argument) / 3 || ('' !== $argument && false !== strpos($alias, $argument))) { $alternatives[$package][] = $alias; } } // First position can only be a package name, not a version if ($alternatives || 0 === $position) { $message = sprintf('"%s" is not a valid alias.', $argument); if ($alternatives) { if (1 === \count($alternatives)) { $message .= " Did you mean this:\n"; } else { $message .= " Did you mean one of these:\n"; } foreach ($alternatives as $package => $aliases) { $message .= sprintf(" \"%s\", supported aliases: \"%s\"\n", $package, implode('", "', $aliases)); } } } else { $message = sprintf('Could not parse version constraint "%s".', $argument); } throw new \UnexpectedValueException($message); } } src/ParallelDownloader.php 0000644 00000022451 15120140521 0011612 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\Config; use Composer\Downloader\TransportException; use Composer\IO\IOInterface; use Composer\Util\RemoteFilesystem; /** * Speedup Composer by downloading packages in parallel. * * @author Nicolas Grekas <p@tchwork.com> */ class ParallelDownloader extends RemoteFilesystem { private $io; private $downloader; private $quiet = true; private $progress = true; private $nextCallback; private $downloadCount; private $nextOptions = []; private $sharedState; private $fileName; private $lastHeaders; public static $cacheNext = false; protected static $cache = []; public function __construct(IOInterface $io, Config $config, array $options = [], $disableTls = false) { $this->io = $io; if (!method_exists(parent::class, 'getRemoteContents')) { $this->io->writeError('Composer >=1.7 not found, downloads will happen in sequence', true, IOInterface::DEBUG); } elseif (!\extension_loaded('curl')) { $this->io->writeError('ext-curl not found, downloads will happen in sequence', true, IOInterface::DEBUG); } else { $this->downloader = new CurlDownloader(); } parent::__construct($io, $config, $options, $disableTls); } public function download(array &$nextArgs, callable $nextCallback, bool $quiet = true, bool $progress = true) { $previousState = [$this->quiet, $this->progress, $this->downloadCount, $this->nextCallback, $this->sharedState]; $this->quiet = $quiet; $this->progress = $progress; $this->downloadCount = \count($nextArgs); $this->nextCallback = $nextCallback; $this->sharedState = (object) [ 'bytesMaxCount' => 0, 'bytesMax' => 0, 'bytesTransferred' => 0, 'nextArgs' => &$nextArgs, 'nestingLevel' => 0, 'maxNestingReached' => false, 'lastProgress' => 0, 'lastUpdate' => microtime(true), ]; if (!$this->quiet) { if (!$this->downloader && method_exists(parent::class, 'getRemoteContents')) { $this->io->writeError('<warning>Enable the "cURL" PHP extension for faster downloads</>'); } $note = ''; if ($this->io->isDecorated()) { $note = '\\' === \DIRECTORY_SEPARATOR ? '' : (false !== stripos(\PHP_OS, 'darwin') ? '🎵' : '🎶'); $note .= $this->downloader ? ('\\' !== \DIRECTORY_SEPARATOR ? ' 💨' : '') : ''; } $this->io->writeError(''); $this->io->writeError(sprintf('<info>Prefetching %d packages</> %s', $this->downloadCount, $note)); $this->io->writeError(' - Downloading', false); if ($this->progress) { $this->io->writeError(' (<comment>0%</>)', false); } } try { $this->getNext(); if ($this->quiet) { // no-op } elseif ($this->progress) { $this->io->overwriteError(' (<comment>100%</>)'); } else { $this->io->writeError(' (<comment>100%</>)'); } } finally { if (!$this->quiet) { $this->io->writeError(''); } list($this->quiet, $this->progress, $this->downloadCount, $this->nextCallback, $this->sharedState) = $previousState; } } public function getOptions(): array { $options = array_replace_recursive(parent::getOptions(), $this->nextOptions); $this->nextOptions = []; return $options; } public function setNextOptions(array $options) { $this->nextOptions = parent::getOptions() !== $options ? $options : []; return $this; } /** * {@inheritdoc} */ public function getLastHeaders(): array { return $this->lastHeaders ?? parent::getLastHeaders(); } /** * {@inheritdoc} */ public function copy($originUrl, $fileUrl, $fileName, $progress = true, $options = []) { $options = array_replace_recursive($this->nextOptions, $options); $this->nextOptions = []; $rfs = clone $this; $rfs->fileName = $fileName; $rfs->progress = $this->progress && $progress; try { return $rfs->get($originUrl, $fileUrl, $options, $fileName, $rfs->progress); } finally { $rfs->lastHeaders = null; $this->lastHeaders = $rfs->getLastHeaders(); } } /** * {@inheritdoc} */ public function getContents($originUrl, $fileUrl, $progress = true, $options = []) { return $this->copy($originUrl, $fileUrl, null, $progress, $options); } /** * @internal */ public function callbackGet($notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax, $nativeDownload = true): void { if (!$nativeDownload && \STREAM_NOTIFY_SEVERITY_ERR === $severity) { throw new TransportException($message, $messageCode); } parent::callbackGet($notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax); if (!$state = $this->sharedState) { return; } if (\STREAM_NOTIFY_FILE_SIZE_IS === $notificationCode) { ++$state->bytesMaxCount; $state->bytesMax += $bytesMax; } if (!$bytesMax || \STREAM_NOTIFY_PROGRESS !== $notificationCode) { if ($state->nextArgs && !$nativeDownload) { $this->getNext(); } return; } if (0 < $state->bytesMax) { $progress = $state->bytesMaxCount / $this->downloadCount; $progress *= 100 * ($state->bytesTransferred + $bytesTransferred) / $state->bytesMax; } else { $progress = 0; } if ($bytesTransferred === $bytesMax) { $state->bytesTransferred += $bytesMax; } if (null !== $state->nextArgs && !$this->quiet && $this->progress && 1 <= $progress - $state->lastProgress) { $progressTime = microtime(true); if (5 <= $progress - $state->lastProgress || 1 <= $progressTime - $state->lastUpdate) { $state->lastProgress = $progress; $this->io->overwriteError(sprintf(' (<comment>%d%%</>)', $progress), false); $state->lastUpdate = microtime(true); } } if (!$nativeDownload || !$state->nextArgs || $bytesTransferred === $bytesMax || $state->maxNestingReached) { return; } if (5 < $state->nestingLevel) { $state->maxNestingReached = true; } else { $this->getNext(); } } /** * {@inheritdoc} */ protected function getRemoteContents($originUrl, $fileUrl, $context, array &$responseHeaders = null, $maxFileSize = null) { if (isset(self::$cache[$fileUrl])) { self::$cacheNext = false; $result = self::$cache[$fileUrl]; if (3 < \func_num_args()) { list($responseHeaders, $result) = $result; } return $result; } if (self::$cacheNext) { self::$cacheNext = false; if (3 < \func_num_args()) { $result = $this->getRemoteContents($originUrl, $fileUrl, $context, $responseHeaders, $maxFileSize); self::$cache[$fileUrl] = [$responseHeaders, $result]; } else { $result = $this->getRemoteContents($originUrl, $fileUrl, $context); self::$cache[$fileUrl] = $result; } return $result; } if (!$this->downloader || !preg_match('/^https?:/', $fileUrl)) { return parent::getRemoteContents($originUrl, $fileUrl, $context, $responseHeaders, $maxFileSize); } try { $result = $this->downloader->get($originUrl, $fileUrl, $context, $this->fileName); if (3 < \func_num_args()) { list($responseHeaders, $result) = $result; } return $result; } catch (TransportException $e) { $this->io->writeError('Retrying download: '.$e->getMessage(), true, IOInterface::DEBUG); return parent::getRemoteContents($originUrl, $fileUrl, $context, $responseHeaders, $maxFileSize); } catch (\Throwable $e) { $responseHeaders = []; throw $e; } } private function getNext() { $state = $this->sharedState; ++$state->nestingLevel; try { while ($state->nextArgs && (!$state->maxNestingReached || 1 === $state->nestingLevel)) { try { $state->maxNestingReached = false; ($this->nextCallback)(...array_shift($state->nextArgs)); } catch (TransportException $e) { $this->io->writeError('Skipping download: '.$e->getMessage(), true, IOInterface::DEBUG); } } } finally { --$state->nestingLevel; } } } src/Path.php 0000644 00000001706 15120140521 0006733 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; /** * @internal */ class Path { private $workingDirectory; public function __construct($workingDirectory) { $this->workingDirectory = $workingDirectory; } public function relativize(string $absolutePath): string { $relativePath = str_replace($this->workingDirectory, '.', $absolutePath); return is_dir($absolutePath) ? rtrim($relativePath, '/').'/' : $relativePath; } public function concatenate(array $parts): string { $first = array_shift($parts); return array_reduce($parts, function (string $initial, string $next): string { return rtrim($initial, '/').'/'.ltrim($next, '/'); }, $first); } } src/Recipe.php 0000644 00000005634 15120140521 0007252 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\Package\PackageInterface; /** * @author Fabien Potencier <fabien@symfony.com> */ class Recipe { private $package; private $name; private $job; private $data; private $lock; public function __construct(PackageInterface $package, string $name, string $job, array $data, array $lock = []) { $this->package = $package; $this->name = $name; $this->job = $job; $this->data = $data; $this->lock = $lock; } public function getPackage(): PackageInterface { return $this->package; } public function getName(): string { return $this->name; } public function getJob(): string { return $this->job; } public function getManifest(): array { if (!isset($this->data['manifest'])) { throw new \LogicException(sprintf('Manifest is not available for recipe "%s".', $this->name)); } return $this->data['manifest']; } public function getFiles(): array { return $this->data['files'] ?? []; } public function getOrigin(): string { return $this->data['origin'] ?? ''; } public function getFormattedOrigin(): string { if (!$this->getOrigin()) { return ''; } // symfony/translation:3.3@github.com/symfony/recipes:branch if (!preg_match('/^([^:]++):([^@]++)@(.+)$/', $this->getOrigin(), $matches)) { return $this->getOrigin(); } return sprintf('<info>%s</> (<comment>>=%s</>): From %s', $matches[1], $matches[2], 'auto-generated recipe' === $matches[3] ? '<comment>'.$matches[3].'</>' : $matches[3]); } public function getURL(): string { if (!$this->data['origin']) { return ''; } // symfony/translation:3.3@github.com/symfony/recipes:branch if (!preg_match('/^([^:]++):([^@]++)@([^:]++):(.+)$/', $this->data['origin'], $matches)) { // that excludes auto-generated recipes, which is what we want return ''; } return sprintf('https://%s/tree/%s/%s/%s', $matches[3], $matches[4], $matches[1], $matches[2]); } public function isContrib(): bool { return $this->data['is_contrib'] ?? false; } public function getRef() { return $this->lock['recipe']['ref'] ?? null; } public function isAuto(): bool { return !isset($this->lock['recipe']); } public function getVersion(): string { return $this->lock['recipe']['version'] ?? $this->lock['version']; } public function getLock(): array { return $this->lock; } } src/Response.php 0000644 00000003717 15120140521 0007641 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; /** * @author Fabien Potencier <fabien@symfony.com> */ class Response implements \JsonSerializable { private $body; private $origHeaders; private $headers; private $code; /** * @param mixed $body The response as JSON */ public function __construct($body, array $headers = [], int $code = 200) { $this->body = $body; $this->origHeaders = $headers; $this->headers = $this->parseHeaders($headers); $this->code = $code; } public function getStatusCode(): int { return $this->code; } public function getHeader(string $name): string { return $this->headers[strtolower($name)][0] ?? ''; } public function getHeaders(string $name): array { return $this->headers[strtolower($name)] ?? []; } public function getBody() { return $this->body; } public function getOrigHeaders(): array { return $this->origHeaders; } public static function fromJson(array $json): self { $response = new self($json['body']); $response->headers = $json['headers']; return $response; } #[\ReturnTypeWillChange] public function jsonSerialize() { return ['body' => $this->body, 'headers' => $this->headers]; } private function parseHeaders(array $headers): array { $values = []; foreach (array_reverse($headers) as $header) { if (preg_match('{^([^:]++):\s*(.+?)\s*$}i', $header, $match)) { $values[strtolower($match[1])][] = $match[2]; } elseif (preg_match('{^HTTP/}i', $header)) { break; } } return $values; } } src/ScriptExecutor.php 0000644 00000012004 15120140521 0011013 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\Composer; use Composer\EventDispatcher\ScriptExecutionException; use Composer\IO\IOInterface; use Composer\Semver\Constraint\EmptyConstraint; use Composer\Semver\Constraint\MatchAllConstraint; use Composer\Util\ProcessExecutor; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Process\PhpExecutableFinder; /** * @author Fabien Potencier <fabien@symfony.com> */ class ScriptExecutor { private $composer; private $io; private $options; private $executor; public function __construct(Composer $composer, IOInterface $io, Options $options, ProcessExecutor $executor = null) { $this->composer = $composer; $this->io = $io; $this->options = $options; $this->executor = $executor ?: new ProcessExecutor(); } /** * @throws ScriptExecutionException if the executed command returns a non-0 exit code */ public function execute(string $type, string $cmd, array $arguments = []) { $parsedCmd = $this->options->expandTargetDir($cmd); if (null === $expandedCmd = $this->expandCmd($type, $parsedCmd, $arguments)) { return; } $cmdOutput = new StreamOutput(fopen('php://temp', 'rw'), OutputInterface::VERBOSITY_VERBOSE, $this->io->isDecorated()); $outputHandler = function ($type, $buffer) use ($cmdOutput) { $cmdOutput->write($buffer, false, OutputInterface::OUTPUT_RAW); }; $this->io->writeError(sprintf('Executing script %s', $parsedCmd), $this->io->isVerbose()); $exitCode = $this->executor->execute($expandedCmd, $outputHandler); $code = 0 === $exitCode ? ' <info>[OK]</>' : ' <error>[KO]</>'; if ($this->io->isVerbose()) { $this->io->writeError(sprintf('Executed script %s %s', $cmd, $code)); } else { $this->io->writeError($code); } if (0 !== $exitCode) { $this->io->writeError(' <error>[KO]</>'); $this->io->writeError(sprintf('<error>Script %s returned with error code %s</>', $cmd, $exitCode)); fseek($cmdOutput->getStream(), 0); foreach (explode("\n", stream_get_contents($cmdOutput->getStream())) as $line) { $this->io->writeError('!! '.$line); } throw new ScriptExecutionException($cmd, $exitCode); } } private function expandCmd(string $type, string $cmd, array $arguments) { switch ($type) { case 'symfony-cmd': return $this->expandSymfonyCmd($cmd, $arguments); case 'php-script': return $this->expandPhpScript($cmd, $arguments); case 'script': return $cmd; default: throw new \InvalidArgumentException(sprintf('Invalid symfony/flex auto-script in composer.json: "%s" is not a valid type of command.', $type)); } } private function expandSymfonyCmd(string $cmd, array $arguments) { $repo = $this->composer->getRepositoryManager()->getLocalRepository(); if (!$repo->findPackage('symfony/console', class_exists(MatchAllConstraint::class) ? new MatchAllConstraint() : new EmptyConstraint())) { $this->io->writeError(sprintf('<warning>Skipping "%s" (needs symfony/console to run).</>', $cmd)); return null; } $console = ProcessExecutor::escape($this->options->get('root-dir').'/'.$this->options->get('bin-dir').'/console'); if ($this->io->isDecorated()) { $console .= ' --ansi'; } return $this->expandPhpScript($console.' '.$cmd, $arguments); } private function expandPhpScript(string $cmd, array $scriptArguments): string { $phpFinder = new PhpExecutableFinder(); if (!$php = $phpFinder->find(false)) { throw new \RuntimeException('The PHP executable could not be found, add it to your PATH and try again.'); } $arguments = $phpFinder->findArguments(); if ($env = (string) getenv('COMPOSER_ORIGINAL_INIS')) { $paths = explode(\PATH_SEPARATOR, $env); $ini = array_shift($paths); } else { $ini = php_ini_loaded_file(); } if ($ini) { $arguments[] = '--php-ini='.$ini; } if ($memoryLimit = (string) getenv('COMPOSER_MEMORY_LIMIT')) { $arguments[] = "-d memory_limit={$memoryLimit}"; } $phpArgs = implode(' ', array_map([ProcessExecutor::class, 'escape'], $arguments)); $scriptArgs = implode(' ', array_map([ProcessExecutor::class, 'escape'], $scriptArguments)); return ProcessExecutor::escape($php).($phpArgs ? ' '.$phpArgs : '').' '.$cmd.($scriptArgs ? ' '.$scriptArgs : ''); } } src/SymfonyBundle.php 0000644 00000007256 15120140521 0010643 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\Composer; use Composer\Package\PackageInterface; /** * @author Fabien Potencier <fabien@symfony.com> */ class SymfonyBundle { private $package; private $operation; private $vendorDir; public function __construct(Composer $composer, PackageInterface $package, string $operation) { $this->package = $package; $this->operation = $operation; $this->vendorDir = rtrim($composer->getConfig()->get('vendor-dir'), '/'); } public function getClassNames(): array { $uninstall = 'uninstall' === $this->operation; $classes = []; $autoload = $this->package->getAutoload(); $isSyliusPlugin = 'sylius-plugin' === $this->package->getType(); foreach (['psr-4' => true, 'psr-0' => false] as $psr => $isPsr4) { if (!isset($autoload[$psr])) { continue; } foreach ($autoload[$psr] as $namespace => $paths) { if (!\is_array($paths)) { $paths = [$paths]; } foreach ($paths as $path) { foreach ($this->extractClassNames($namespace, $isSyliusPlugin) as $class) { // we only check class existence on install as we do have the code available // in contrast to uninstall operation if (!$uninstall && !$this->isBundleClass($class, $path, $isPsr4)) { continue; } $classes[] = $class; } } } } return $classes; } private function extractClassNames(string $namespace, bool $isSyliusPlugin): array { $namespace = trim($namespace, '\\'); $class = $namespace.'\\'; $parts = explode('\\', $namespace); $suffix = $parts[\count($parts) - 1]; $endOfWord = substr($suffix, -6); if ($isSyliusPlugin) { if ('Bundle' !== $endOfWord && 'Plugin' !== $endOfWord) { $suffix .= 'Bundle'; } } elseif ('Bundle' !== $endOfWord) { $suffix .= 'Bundle'; } $classes = [$class.$suffix]; $acc = ''; foreach (\array_slice($parts, 0, -1) as $part) { if ('Bundle' === $part || ($isSyliusPlugin && 'Plugin' === $part)) { continue; } $classes[] = $class.$part.$suffix; $acc .= $part; $classes[] = $class.$acc.$suffix; } return array_unique($classes); } private function isBundleClass(string $class, string $path, bool $isPsr4): bool { $classPath = ($this->vendorDir ? $this->vendorDir.'/' : '').$this->package->getPrettyName().'/'.$path.'/'; $parts = explode('\\', $class); $class = $parts[\count($parts) - 1]; if (!$isPsr4) { $classPath .= str_replace('\\', '', implode('/', \array_slice($parts, 0, -1))).'/'; } $classPath .= str_replace('\\', '/', $class).'.php'; if (!file_exists($classPath)) { return false; } // heuristic that should work in almost all cases $classContents = file_get_contents($classPath); return (false !== strpos($classContents, 'Symfony\Component\HttpKernel\Bundle\Bundle')) || (false !== strpos($classContents, 'Symfony\Component\HttpKernel\Bundle\AbstractBundle')); } } src/TruncatedComposerRepository.php 0000644 00000003012 15120140521 0013570 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\Config; use Composer\EventDispatcher\EventDispatcher; use Composer\IO\IOInterface; use Composer\Package\RootPackageInterface; use Composer\Repository\ComposerRepository as BaseComposerRepository; use Composer\Util\RemoteFilesystem; /** * @author Nicolas Grekas <p@tchwork.com> */ class TruncatedComposerRepository extends BaseComposerRepository { public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null) { parent::__construct($repoConfig, $io, $config, $eventDispatcher, $rfs); $this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$'); } public function setSymfonyRequire(string $symfonyRequire, RootPackageInterface $rootPackage, Downloader $downloader, IOInterface $io) { $this->cache->setSymfonyRequire($symfonyRequire, $rootPackage, $downloader, $io); } protected function fetchFile($filename, $cacheKey = null, $sha256 = null, $storeLastModifiedTime = false) { $data = parent::fetchFile($filename, $cacheKey, $sha256, $storeLastModifiedTime); return \is_array($data) ? $this->cache->removeLegacyTags($data) : $data; } } src/Unpacker.php 0000644 00000021162 15120140521 0007605 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Flex; use Composer\Composer; use Composer\Config\JsonConfigSource; use Composer\DependencyResolver\Pool; use Composer\Factory; use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; use Composer\Package\Locker; use Composer\Package\Version\VersionSelector; use Composer\Plugin\PluginInterface; use Composer\Repository\CompositeRepository; use Composer\Repository\RepositorySet; use Composer\Semver\VersionParser; use Symfony\Flex\Unpack\Operation; use Symfony\Flex\Unpack\Result; class Unpacker { private $composer; private $resolver; private $dryRun; private $versionParser; public function __construct(Composer $composer, PackageResolver $resolver, bool $dryRun) { $this->composer = $composer; $this->resolver = $resolver; $this->dryRun = $dryRun; $this->versionParser = new VersionParser(); } public function unpack(Operation $op, Result $result = null, &$links = [], bool $devRequire = false): Result { if (null === $result) { $result = new Result(); } $localRepo = $this->composer->getRepositoryManager()->getLocalRepository(); foreach ($op->getPackages() as $package) { $pkg = $localRepo->findPackage($package['name'], '*'); $pkg = $pkg ?? $this->composer->getRepositoryManager()->findPackage($package['name'], $package['version'] ?: '*'); // not unpackable or no --unpack flag or empty packs (markers) if ( null === $pkg || 'symfony-pack' !== $pkg->getType() || !$op->shouldUnpack() || 0 === \count($pkg->getRequires()) + \count($pkg->getDevRequires()) ) { $result->addRequired($package['name'].($package['version'] ? ':'.$package['version'] : '')); continue; } if (!$result->addUnpacked($pkg)) { continue; } $requires = []; foreach ($pkg->getRequires() as $link) { $requires[$link->getTarget()] = $link; } $devRequires = $pkg->getDevRequires(); foreach ($devRequires as $i => $link) { if (!isset($requires[$link->getTarget()])) { throw new \RuntimeException(sprintf('Symfony pack "%s" must duplicate all entries from "require-dev" into "require" but entry "%s" was not found.', $package['name'], $link->getTarget())); } $devRequires[$i] = $requires[$link->getTarget()]; unset($requires[$link->getTarget()]); } $versionSelector = null; foreach ([$requires, $devRequires] as $dev => $requires) { $dev = $dev ?: $devRequire ?: $package['dev']; foreach ($requires as $link) { if ('php' === $linkName = $link->getTarget()) { continue; } $constraint = $link->getPrettyConstraint(); $constraint = substr($this->resolver->parseVersion($linkName, $constraint, true), 1) ?: $constraint; if ($subPkg = $localRepo->findPackage($linkName, '*')) { if ('symfony-pack' === $subPkg->getType()) { $subOp = new Operation(true, $op->shouldSort()); $subOp->addPackage($subPkg->getName(), $constraint, $dev); $result = $this->unpack($subOp, $result, $links, $dev); continue; } if ('*' === $constraint) { if (null === $versionSelector) { $pool = class_exists(RepositorySet::class) ? RepositorySet::class : Pool::class; $pool = new $pool($this->composer->getPackage()->getMinimumStability(), $this->composer->getPackage()->getStabilityFlags()); $pool->addRepository(new CompositeRepository($this->composer->getRepositoryManager()->getRepositories())); $versionSelector = new VersionSelector($pool); } $constraint = $versionSelector->findRecommendedRequireVersion($subPkg); } } $linkType = $dev ? 'require-dev' : 'require'; $constraint = $this->versionParser->parseConstraints($constraint); if (isset($links[$linkName])) { $links[$linkName]['constraints'][] = $constraint; if ('require' === $linkType) { $links[$linkName]['type'] = 'require'; } } else { $links[$linkName] = [ 'type' => $linkType, 'name' => $linkName, 'constraints' => [$constraint], ]; } } } } if ($this->dryRun || 1 < \func_num_args()) { return $result; } $jsonPath = Factory::getComposerFile(); $jsonContent = file_get_contents($jsonPath); $jsonStored = json_decode($jsonContent, true); $jsonManipulator = new JsonManipulator($jsonContent); foreach ($links as $link) { // nothing to do, package is already present in the "require" section if (isset($jsonStored['require'][$link['name']])) { continue; } if (isset($jsonStored['require-dev'][$link['name']])) { // nothing to do, package is already present in the "require-dev" section if ('require-dev' === $link['type']) { continue; } // removes package from "require-dev", because it will be moved to "require" // save stored constraint $link['constraints'][] = $this->versionParser->parseConstraints($jsonStored['require-dev'][$link['name']]); $jsonManipulator->removeSubNode('require-dev', $link['name']); } $constraint = end($link['constraints']); if (!$jsonManipulator->addLink($link['type'], $link['name'], $constraint->getPrettyString(), $op->shouldSort())) { throw new \RuntimeException(sprintf('Unable to unpack package "%s".', $link['name'])); } } file_put_contents($jsonPath, $jsonManipulator->getContents()); return $result; } public function updateLock(Result $result, IOInterface $io): void { $json = new JsonFile(Factory::getComposerFile()); $manipulator = new JsonConfigSource($json); $locker = $this->composer->getLocker(); $lockData = $locker->getLockData(); foreach ($result->getUnpacked() as $package) { $manipulator->removeLink('require-dev', $package->getName()); foreach ($lockData['packages-dev'] as $i => $pkg) { if ($package->getName() === $pkg['name']) { unset($lockData['packages-dev'][$i]); } } $manipulator->removeLink('require', $package->getName()); foreach ($lockData['packages'] as $i => $pkg) { if ($package->getName() === $pkg['name']) { unset($lockData['packages'][$i]); } } } $jsonContent = file_get_contents($json->getPath()); $lockData['packages'] = array_values($lockData['packages']); $lockData['packages-dev'] = array_values($lockData['packages-dev']); $lockData['content-hash'] = Locker::getContentHash($jsonContent); $lockFile = new JsonFile(substr($json->getPath(), 0, -4).'lock', null, $io); if (!$this->dryRun) { $lockFile->write($lockData); } // force removal of files under vendor/ if (version_compare('2.0.0', PluginInterface::PLUGIN_API_VERSION, '>')) { $locker = new Locker($io, $lockFile, $this->composer->getRepositoryManager(), $this->composer->getInstallationManager(), $jsonContent); } else { $locker = new Locker($io, $lockFile, $this->composer->getInstallationManager(), $jsonContent); } $this->composer->setLocker($locker); } } LICENSE 0000644 00000002051 15120140521 0005536 0 ustar 00 Copyright (c) 2016-2019 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. README.md 0000644 00000000576 15120140521 0006022 0 ustar 00 <p align="center"><a href="https://symfony.com" target="_blank"> <img src="https://symfony.com/logos/symfony_black_02.svg"> </a></p> [Symfony Flex][1] helps developers create [Symfony][2] applications, from the most simple micro-style projects to the more complex ones with dozens of dependencies. [1]: https://symfony.com/doc/current/setup/flex.html [2]: https://symfony.com composer.json 0000644 00000001446 15120140521 0007262 0 ustar 00 { "name": "symfony/flex", "type": "composer-plugin", "description": "Composer plugin for Symfony", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien.potencier@gmail.com" } ], "minimum-stability": "dev", "require": { "php": ">=7.1", "composer-plugin-api": "^1.0|^2.0" }, "require-dev": { "composer/composer": "^1.0.2|^2.0", "symfony/dotenv": "^4.4|^5.0|^6.0", "symfony/filesystem": "^4.4|^5.0|^6.0", "symfony/phpunit-bridge": "^4.4.12|^5.0|^6.0", "symfony/process": "^4.4|^5.0|^6.0" }, "autoload": { "psr-4": { "Symfony\\Flex\\": "src" } }, "extra": { "class": "Symfony\\Flex\\Flex" } }
Coded With 💗 by
0x6ick