commit 99d46ae4ae56499c0eada1950ac30f7612b83335 Author: olaf Date: Thu Nov 14 13:46:24 2024 +0100 initial commit diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6ab9912 --- /dev/null +++ b/composer.json @@ -0,0 +1,65 @@ +{ + "type": "project", + "license": "proprietary", + "minimum-stability": "stable", + "prefer-stable": true, + "require": { + "php": ">=8.1", + "ext-ctype": "*", + "ext-iconv": "*", + "symfony/console": "6.4.*", + "symfony/dotenv": "6.4.*", + "symfony/flex": "^2", + "symfony/framework-bundle": "6.4.*", + "symfony/runtime": "6.4.*", + "symfony/yaml": "6.4.*" + }, + "require-dev": { + }, + "config": { + "allow-plugins": { + "php-http/discovery": true, + "symfony/flex": true, + "symfony/runtime": true + }, + "sort-packages": true + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "replace": { + "symfony/polyfill-ctype": "*", + "symfony/polyfill-iconv": "*", + "symfony/polyfill-php72": "*", + "symfony/polyfill-php73": "*", + "symfony/polyfill-php74": "*", + "symfony/polyfill-php80": "*", + "symfony/polyfill-php81": "*" + }, + "scripts": { + "auto-scripts": [ + ], + "post-install-cmd": [ + "@auto-scripts" + ], + "post-update-cmd": [ + "@auto-scripts" + ] + }, + "conflict": { + "symfony/symfony": "*" + }, + "extra": { + "symfony": { + "allow-contrib": false, + "require": "6.4.*" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..c0f6a33 --- /dev/null +++ b/composer.lock @@ -0,0 +1,91 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "6c7e3dbdfc97c282707609d966719896", + "packages": [ + { + "name": "symfony/flex", + "version": "v2.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/flex.git", + "reference": "92f4fba342161ff36072bd3b8e0b3c6c23160402" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/flex/zipball/92f4fba342161ff36072bd3b8e0b3c6c23160402", + "reference": "92f4fba342161ff36072bd3b8e0b3c6c23160402", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.1", + "php": ">=8.0" + }, + "conflict": { + "composer/semver": "<1.7.2" + }, + "require-dev": { + "composer/composer": "^2.1", + "symfony/dotenv": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Flex\\Flex" + }, + "autoload": { + "psr-4": { + "Symfony\\Flex\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien.potencier@gmail.com" + } + ], + "description": "Composer plugin for Symfony", + "support": { + "issues": "https://github.com/symfony/flex/issues", + "source": "https://github.com/symfony/flex/tree/v2.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-07T08:51:54+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=8.1", + "ext-ctype": "*", + "ext-iconv": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 0000000..718fd6e --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,25 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ + private $vendorDir; + + // PSR-4 + /** + * @var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var list + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array>> + */ + private $prefixesPsr0 = array(); + /** + * @var list + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var array + */ + private $missingClasses = array(); + + /** @var string|null */ + private $apcuPrefix; + + /** + * @var array + */ + private static $registeredLoaders = array(); + + /** + * @param string|null $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return array> + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return list + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return list + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return array Array of classname => path + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + $includeFile = self::$includeFile; + $includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders keyed by their corresponding vendor directories. + * + * @return array + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } + + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } +} diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php new file mode 100644 index 0000000..51e734a --- /dev/null +++ b/vendor/composer/InstalledVersions.php @@ -0,0 +1,359 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + $installed[] = self::$installedByVendor[$vendorDir] = $required; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array()) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +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. + diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..0fb0a2c --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,10 @@ + $vendorDir . '/composer/InstalledVersions.php', +); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..15a2ff3 --- /dev/null +++ b/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/symfony/flex/src'), + 'App\\Tests\\' => array($baseDir . '/tests'), + 'App\\' => array($baseDir . '/src'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 0000000..3d4b1a5 --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,38 @@ +register(true); + + return $loader; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 0000000..c6d9b7d --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,49 @@ + + array ( + 'Symfony\\Flex\\' => 13, + ), + 'A' => + array ( + 'App\\Tests\\' => 10, + 'App\\' => 4, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Symfony\\Flex\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/flex/src', + ), + 'App\\Tests\\' => + array ( + 0 => __DIR__ . '/../..' . '/tests', + ), + 'App\\' => + array ( + 0 => __DIR__ . '/../..' . '/src', + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit54c05e64f01f49eb136e9af7b3075bdd::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit54c05e64f01f49eb136e9af7b3075bdd::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit54c05e64f01f49eb136e9af7b3075bdd::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 0000000..cd22c4d --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,77 @@ +{ + "packages": [ + { + "name": "symfony/flex", + "version": "v2.4.7", + "version_normalized": "2.4.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/flex.git", + "reference": "92f4fba342161ff36072bd3b8e0b3c6c23160402" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/flex/zipball/92f4fba342161ff36072bd3b8e0b3c6c23160402", + "reference": "92f4fba342161ff36072bd3b8e0b3c6c23160402", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.1", + "php": ">=8.0" + }, + "conflict": { + "composer/semver": "<1.7.2" + }, + "require-dev": { + "composer/composer": "^2.1", + "symfony/dotenv": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0" + }, + "time": "2024-10-07T08:51:54+00:00", + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Flex\\Flex" + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Flex\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien.potencier@gmail.com" + } + ], + "description": "Composer plugin for Symfony", + "support": { + "issues": "https://github.com/symfony/flex/issues", + "source": "https://github.com/symfony/flex/tree/v2.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/flex" + } + ], + "dev": true, + "dev-package-names": [] +} diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php new file mode 100644 index 0000000..36aae14 --- /dev/null +++ b/vendor/composer/installed.php @@ -0,0 +1,74 @@ + array( + 'name' => 'symfony/skeleton', + 'pretty_version' => 'v6.4.99', + 'version' => '6.4.99.0', + 'reference' => null, + 'type' => 'project', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev' => true, + ), + 'versions' => array( + 'symfony/flex' => array( + 'pretty_version' => 'v2.4.7', + 'version' => '2.4.7.0', + 'reference' => '92f4fba342161ff36072bd3b8e0b3c6c23160402', + 'type' => 'composer-plugin', + 'install_path' => __DIR__ . '/../symfony/flex', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-ctype' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'symfony/polyfill-iconv' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'symfony/polyfill-php72' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'symfony/polyfill-php73' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'symfony/polyfill-php74' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'symfony/polyfill-php80' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'symfony/polyfill-php81' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'symfony/skeleton' => array( + 'pretty_version' => 'v6.4.99', + 'version' => '6.4.99.0', + 'reference' => null, + 'type' => 'project', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php new file mode 100644 index 0000000..4c3a5d6 --- /dev/null +++ b/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ += 80100)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} diff --git a/vendor/symfony/flex/LICENSE b/vendor/symfony/flex/LICENSE new file mode 100644 index 0000000..3c464ca --- /dev/null +++ b/vendor/symfony/flex/LICENSE @@ -0,0 +1,19 @@ +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. diff --git a/vendor/symfony/flex/README.md b/vendor/symfony/flex/README.md new file mode 100644 index 0000000..1102474 --- /dev/null +++ b/vendor/symfony/flex/README.md @@ -0,0 +1,10 @@ +

+ +

+ +[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 diff --git a/vendor/symfony/flex/composer.json b/vendor/symfony/flex/composer.json new file mode 100644 index 0000000..d6b8f85 --- /dev/null +++ b/vendor/symfony/flex/composer.json @@ -0,0 +1,35 @@ +{ + "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": ">=8.0", + "composer-plugin-api": "^2.1" + }, + "require-dev": { + "composer/composer": "^2.1", + "symfony/dotenv": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0" + }, + "conflict": { + "composer/semver": "<1.7.2" + }, + "autoload": { + "psr-4": { + "Symfony\\Flex\\": "src" + } + }, + "extra": { + "class": "Symfony\\Flex\\Flex" + } +} diff --git a/vendor/symfony/flex/src/Command/DumpEnvCommand.php b/vendor/symfony/flex/src/Command/DumpEnvCommand.php new file mode 100644 index 0000000..6f7ed6f --- /dev/null +++ b/vendor/symfony/flex/src/Command/DumpEnvCommand.php @@ -0,0 +1,147 @@ + + * + * 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 = <<getIO()->writeError('Successfully dumped .env files in .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; + } +} diff --git a/vendor/symfony/flex/src/Command/InstallRecipesCommand.php b/vendor/symfony/flex/src/Command/InstallRecipesCommand.php new file mode 100644 index 0000000..d1ae1a4 --- /dev/null +++ b/vendor/symfony/flex/src/Command/InstallRecipesCommand.php @@ -0,0 +1,181 @@ + + * + * 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 -v to see more details', + '', + ]); + } + + if ($targetPackages = $input->getArgument('packages')) { + if ($invalidPackages = array_diff($targetPackages, $totalPackages)) { + $io->writeError(sprintf('Cannot update: some packages are not installed: %s', implode(', ', $invalidPackages))); + + return 1; + } + + if ($packagesRequiringForce = array_diff($targetPackages, $packages)) { + $io->writeError(sprintf('Recipe(s) already installed for: %s', implode(', ', $packagesRequiringForce))); + $io->writeError('Re-run the command with --force 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('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 = [ + '', + ' ', + ' Files have been reset to the latest version of the recipe. ', + ' ', + '', + ' * Use 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 git add -p and 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 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(' git mv %s %s && %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 git clean --dry-run'; + $output[] = ' Add the new files you want to keep using git add'; + $output[] = ' then delete the rest using git clean --force'; + $output[] = ''; + + $io->write($output); + } + + return 0; + } +} diff --git a/vendor/symfony/flex/src/Command/RecipesCommand.php b/vendor/symfony/flex/src/Command/RecipesCommand.php new file mode 100644 index 0000000..3cdac81 --- /dev/null +++ b/vendor/symfony/flex/src/Command/RecipesCommand.php @@ -0,0 +1,344 @@ + + * + * 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 Composer\Util\HttpDownloader; +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 + */ +class RecipesCommand extends BaseCommand +{ + /** @var \Symfony\Flex\Flex */ + private $flex; + + private Lock $symfonyLock; + private GithubApi $githubApi; + + public function __construct(/* cannot be type-hinted */ $flex, Lock $symfonyLock, HttpDownloader $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): int + { + $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('Package %s is not installed', $name)); + + continue; + } + + $operations[] = new InformationOperation($pkg); + } + + $recipes = $this->flex->fetchRecipes($operations, false); + ksort($recipes); + + $nbRecipe = \count($recipes); + if ($nbRecipe <= 0) { + $this->getIO()->writeError('No recipe found'); + + 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; + foreach ($recipes as $name => $recipe) { + $lockRef = $this->symfonyLock->get($name)['recipe']['ref'] ?? null; + + $additional = null; + if (null === $lockRef && null !== $recipe->getRef()) { + $additional = '(recipe not installed)'; + } elseif ($recipe->getRef() !== $lockRef && !$recipe->isAuto()) { + $additional = '(update available)'; + } + + 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([ + '', + ' ', + sprintf(' %s recipes. ', $outdated ? ' Outdated' : 'Available'), + ' ', + '', + ], $write, [ + '', + 'Run:', + ' * composer recipes vendor/package to see details about a recipe.', + ' * composer recipes:update vendor/package 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 = 'up to date'; + if ($recipe->isAuto()) { + $status = 'auto-generated recipe'; + } elseif (null === $lockRef && null !== $recipe->getRef()) { + $status = 'recipe not installed'; + } elseif ($recipe->getRef() !== $lockRef) { + $status = 'update available'; + } + + $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('name : '.$recipe->getName()); + $io->write('version : '.($lockVersion ?? 'n/a')); + $io->write('status : '.$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('installed recipe : '.$recipeUrl); + } + + if ($lockRef !== $recipe->getRef()) { + $io->write('latest recipe : '.$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='; + $historyUrl .= (new \DateTime($commitDate)) + ->setTimezone(new \DateTimeZone('UTC')) + ->modify('+1 seconds') + ->format('Y-m-d\TH:i:s\Z'); + } + + $io->write('recipe history : '.$historyUrl); + } + + if (null !== $lockFiles) { + $io->write('files : '); + $io->write(''); + + $tree = $this->generateFilesTree($lockFiles); + + $this->displayFilesTree($tree); + } + + if ($lockRef !== $recipe->getRef()) { + $io->write([ + '', + 'Update this recipe by running:', + sprintf('composer recipes:update %s', $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); + } +} diff --git a/vendor/symfony/flex/src/Command/UpdateRecipesCommand.php b/vendor/symfony/flex/src/Command/UpdateRecipesCommand.php new file mode 100644 index 0000000..15296ef --- /dev/null +++ b/vendor/symfony/flex/src/Command/UpdateRecipesCommand.php @@ -0,0 +1,415 @@ + + * + * 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 recipes:update: 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 composer recipes:install %s --force -v 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(' composer recipes:install %s --force -v', $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(' composer recipes:install %s --force -v', $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(' composer recipes:install %s --force -v', $packageName), + ]); + + return 1; + } + + $newRecipe = $this->getRecipe($package); + + if ($newRecipe->getRef() === $originalRecipe->getRef()) { + $io->write(sprintf('This recipe for %s is already at the latest version.', $packageName)); + + return 0; + } + + $io->write([ + sprintf(' Updating recipe for %s...', $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([ + 'There was an error applying the recipe update patch', + $throwable->getMessage(), + '', + 'Update the recipe by re-installing the latest version with:', + sprintf(' composer recipes:install %s --force -v', $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([ + ' ', + ' Yes! Recipe updated! ', + ' ', + '', + ]); + + if ($hasConflicts) { + $io->write([ + ' The recipe was updated but with one or more conflicts.', + ' Run git status 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 git status or git diff --cached to see the changes.', + ' When you\'re ready, commit these changes like normal.', + ]); + } + } + + if (0 !== \count($recipeUpdate->getCopyFromPackagePaths())) { + $io->write([ + '', + ' 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 %s 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(' * %s', $filename); + } + } + $io->write([ + '', + ' 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 %s', $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: 0)'; + + $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; + } +} diff --git a/vendor/symfony/flex/src/Configurator.php b/vendor/symfony/flex/src/Configurator.php new file mode 100644 index 0000000..dbffb10 --- /dev/null +++ b/vendor/symfony/flex/src/Configurator.php @@ -0,0 +1,119 @@ + + * + * 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 + */ +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, + 'dotenv' => Configurator\DotenvConfigurator::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); + } +} diff --git a/vendor/symfony/flex/src/Configurator/AbstractConfigurator.php b/vendor/symfony/flex/src/Configurator/AbstractConfigurator.php new file mode 100644 index 0000000..aea4dda --- /dev/null +++ b/vendor/symfony/flex/src/Configurator/AbstractConfigurator.php @@ -0,0 +1,131 @@ + + * + * 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 + */ +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', $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; + } +} diff --git a/vendor/symfony/flex/src/Configurator/AddLinesConfigurator.php b/vendor/symfony/flex/src/Configurator/AddLinesConfigurator.php new file mode 100644 index 0000000..146b28b --- /dev/null +++ b/vendor/symfony/flex/src/Configurator/AddLinesConfigurator.php @@ -0,0 +1,270 @@ + + * @author Ryan Weaver + */ +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, + ]; + + /** + * Holds file contents for files that have been loaded. + * This allows us to "change" the contents of a file multiple + * times before we actually write it out. + * + * @var string[] + */ + private $fileContents = []; + + public function configure(Recipe $recipe, $config, Lock $lock, array $options = []): void + { + $this->fileContents = []; + $this->executeConfigure($recipe, $config); + + foreach ($this->fileContents as $file => $contents) { + $this->write(sprintf('[add-lines] Patching file "%s"', $this->relativize($file))); + file_put_contents($file, $contents); + } + } + + public function unconfigure(Recipe $recipe, $config, Lock $lock): void + { + $this->fileContents = []; + $this->executeUnconfigure($recipe, $config); + + foreach ($this->fileContents as $file => $change) { + $this->write(sprintf('[add-lines] Reverting file "%s"', $this->relativize($file))); + file_put_contents($file, $change); + } + } + + public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void + { + // manually check for "requires", as unconfigure ignores it + $originalConfig = array_filter($originalConfig, function ($item) { + return !isset($item['requires']) || $this->isPackageInstalled($item['requires']); + }); + + // reset the file content cache + $this->fileContents = []; + $this->executeUnconfigure($recipeUpdate->getOriginalRecipe(), $originalConfig); + $this->executeConfigure($recipeUpdate->getNewRecipe(), $newConfig); + $newFiles = []; + $originalFiles = []; + foreach ($this->fileContents as $file => $contents) { + // set the original file to the current contents + $originalFiles[$this->relativize($file)] = file_get_contents($file); + // and the new file where the old recipe was unconfigured, and the new configured + $newFiles[$this->relativize($file)] = $contents; + } + $recipeUpdate->addOriginalFiles($originalFiles); + $recipeUpdate->addNewFiles($newFiles); + } + + public function executeConfigure(Recipe $recipe, $config): 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'), $this->options->expandTargetDir($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 %s as it does not exist. Missing lines:', $patch['file']), + '"""', + $content, + '"""', + '', + ], $warnIfMissing ? IOInterface::NORMAL : IOInterface::VERBOSE); + + continue; + } + + 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; + + $newContents = $this->getPatchedContents($file, $content, $position, $target, $warnIfMissing); + $this->fileContents[$file] = $newContents; + } + } + + public function executeUnconfigure(Recipe $recipe, $config): 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'), $this->options->expandTargetDir($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']; + + $newContents = $this->getUnPatchedContents($file, $value); + $this->fileContents[$file] = $newContents; + } + } + + private function getPatchedContents(string $file, string $value, string $position, ?string $target, bool $warnIfMissing): string + { + $fileContents = $this->readFile($file); + + if (false !== strpos($fileContents, $value)) { + return $fileContents; // 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), + '"""', + $value, + '"""', + '', + ], $warnIfMissing ? IOInterface::NORMAL : IOInterface::VERBOSE); + } + + break; + } + + return $fileContents; + } + + private function getUnPatchedContents(string $file, $value): string + { + $fileContents = $this->readFile($file); + + if (false === strpos($fileContents, $value)) { + return $fileContents; // 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); + + return substr_replace($fileContents, '', $position, \strlen($value)); + } + + 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; + } + + private function relativize(string $path): string + { + $rootDir = $this->options->get('root-dir'); + if (0 === strpos($path, $rootDir)) { + $path = substr($path, \strlen($rootDir) + 1); + } + + return ltrim($path, '/\\'); + } + + private function readFile(string $file): string + { + if (isset($this->fileContents[$file])) { + return $this->fileContents[$file]; + } + + $fileContents = file_get_contents($file); + $this->fileContents[$file] = $fileContents; + + return $fileContents; + } +} diff --git a/vendor/symfony/flex/src/Configurator/BundlesConfigurator.php b/vendor/symfony/flex/src/Configurator/BundlesConfigurator.php new file mode 100644 index 0000000..2aab5e7 --- /dev/null +++ b/vendor/symfony/flex/src/Configurator/BundlesConfigurator.php @@ -0,0 +1,150 @@ + + * + * 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 + */ +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 = " $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'); + } +} diff --git a/vendor/symfony/flex/src/Configurator/ComposerScriptsConfigurator.php b/vendor/symfony/flex/src/Configurator/ComposerScriptsConfigurator.php new file mode 100644 index 0000000..abdcefc --- /dev/null +++ b/vendor/symfony/flex/src/Configurator/ComposerScriptsConfigurator.php @@ -0,0 +1,75 @@ + + * + * 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 + */ +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(); + } +} diff --git a/vendor/symfony/flex/src/Configurator/ContainerConfigurator.php b/vendor/symfony/flex/src/Configurator/ContainerConfigurator.php new file mode 100644 index 0000000..e501a09 --- /dev/null +++ b/vendor/symfony/flex/src/Configurator/ContainerConfigurator.php @@ -0,0 +1,164 @@ + + * + * 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 + */ +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'); + } +} diff --git a/vendor/symfony/flex/src/Configurator/CopyFromPackageConfigurator.php b/vendor/symfony/flex/src/Configurator/CopyFromPackageConfigurator.php new file mode 100644 index 0000000..9d28fa2 --- /dev/null +++ b/vendor/symfony/flex/src/Configurator/CopyFromPackageConfigurator.php @@ -0,0 +1,169 @@ + + * + * 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 + */ +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 "%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 "%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 "%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 "%s"', $this->path->relativize($targetPath))); + } else { + @unlink($targetPath); + $this->write(sprintf(' Removed "%s"', $this->path->relativize($targetPath))); + } + } + } + + private function createSourceIterator(string $source, int $mode): \RecursiveIteratorIterator + { + return new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS), $mode); + } +} diff --git a/vendor/symfony/flex/src/Configurator/CopyFromRecipeConfigurator.php b/vendor/symfony/flex/src/Configurator/CopyFromRecipeConfigurator.php new file mode 100644 index 0000000..b423311 --- /dev/null +++ b/vendor/symfony/flex/src/Configurator/CopyFromRecipeConfigurator.php @@ -0,0 +1,175 @@ + + * + * 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 + */ +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 "%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 "%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); + } +} diff --git a/vendor/symfony/flex/src/Configurator/DockerComposeConfigurator.php b/vendor/symfony/flex/src/Configurator/DockerComposeConfigurator.php new file mode 100644 index 0000000..42bdda9 --- /dev/null +++ b/vendor/symfony/flex/src/Configurator/DockerComposeConfigurator.php @@ -0,0 +1,404 @@ + + * + * 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 compose.yaml file. + * + * @author Kévin Dunglas + */ +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 $key => $val) { + // Support for the short recipe syntax that modifies compose.yaml only + if (isset($val[0])) { + return ['compose.yaml' => $config]; + } + + if (!str_starts_with($key, 'docker-')) { + continue; + } + + // If the recipe still use the legacy "docker-compose.yml" names, remove the "docker-" prefix and change the extension + $newKey = pathinfo(substr($key, 7), \PATHINFO_FILENAME).'.yaml'; + $config[$newKey] = $val; + unset($config[$key]); + } + + 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'])) { + $filenameToFind = pathinfo($file, \PATHINFO_FILENAME); + $separator = $_SERVER['COMPOSE_PATH_SEPARATOR'] ?? ('\\' === \DIRECTORY_SEPARATOR ? ';' : ':'); + + $files = explode($separator, $_SERVER['COMPOSE_FILE']); + foreach ($files as $f) { + $filename = pathinfo($f, \PATHINFO_FILENAME); + if ($filename !== $filenameToFind && "docker-$filenameToFind" !== $filename) { + 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 { + if ( + $this->filesystem->exists($dockerComposeFile = sprintf('%s/%s', $dir, $file)) || + // Test with the ".yml" extension if the file doesn't end up with ".yaml" + $this->filesystem->exists($dockerComposeFile = substr($dockerComposeFile, 0, -3).'ml') || + // Test with the legacy "docker-" suffix if "compose.ya?ml" doesn't exist + $this->filesystem->exists($dockerComposeFile = sprintf('%s/docker-%s', $dir, $file)) || + $this->filesystem->exists($dockerComposeFile = substr($dockerComposeFile, 0, -3).'ml') + ) { + 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, ''); + $this->write(sprintf(' Created "%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 = $file; + if (0 === strpos($file, $rootDir)) { + $localPath = substr($file, \strlen($rootDir) + 1); + } + $localPath = ltrim($localPath, '/\\'); + $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(' - %s %s', $warning, $recipe->getFormattedOrigin())); + $question = ' The recipe for this package contains some Docker configuration. + + This may create/update compose.yaml or update Dockerfile (if it exists). + + Do you want to include Docker configuration from recipes? + [y] Yes + [n] No + [p] Yes permanently, never ask again for this project + [x] No permanently, never ask again for this project + (defaults to 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' + ); + } +} diff --git a/vendor/symfony/flex/src/Configurator/DockerfileConfigurator.php b/vendor/symfony/flex/src/Configurator/DockerfileConfigurator.php new file mode 100644 index 0000000..423cf9c --- /dev/null +++ b/vendor/symfony/flex/src/Configurator/DockerfileConfigurator.php @@ -0,0 +1,125 @@ + + * + * 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 + */ +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; + } +} diff --git a/vendor/symfony/flex/src/Configurator/DotenvConfigurator.php b/vendor/symfony/flex/src/Configurator/DotenvConfigurator.php new file mode 100644 index 0000000..1ee1271 --- /dev/null +++ b/vendor/symfony/flex/src/Configurator/DotenvConfigurator.php @@ -0,0 +1,50 @@ + + * + * 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; + +class DotenvConfigurator extends AbstractConfigurator +{ + public function configure(Recipe $recipe, $vars, Lock $lock, array $options = []) + { + foreach ($vars as $suffix => $vars) { + $configurator = new EnvConfigurator($this->composer, $this->io, $this->options, $suffix); + $configurator->configure($recipe, $vars, $lock, $options); + } + } + + public function unconfigure(Recipe $recipe, $vars, Lock $lock) + { + foreach ($vars as $suffix => $vars) { + $configurator = new EnvConfigurator($this->composer, $this->io, $this->options, $suffix); + $configurator->unconfigure($recipe, $vars, $lock); + } + } + + public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void + { + foreach ($originalConfig as $suffix => $vars) { + $configurator = new EnvConfigurator($this->composer, $this->io, $this->options, $suffix); + $configurator->update($recipeUpdate, $vars, $newConfig[$suffix] ?? []); + } + + foreach ($newConfig as $suffix => $vars) { + if (!isset($originalConfig[$suffix])) { + $configurator = new EnvConfigurator($this->composer, $this->io, $this->options, $suffix); + $configurator->update($recipeUpdate, [], $vars); + } + } + } +} diff --git a/vendor/symfony/flex/src/Configurator/EnvConfigurator.php b/vendor/symfony/flex/src/Configurator/EnvConfigurator.php new file mode 100644 index 0000000..baddec9 --- /dev/null +++ b/vendor/symfony/flex/src/Configurator/EnvConfigurator.php @@ -0,0 +1,295 @@ + + * + * 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\Recipe; +use Symfony\Flex\Update\RecipeUpdate; + +/** + * @author Fabien Potencier + */ +class EnvConfigurator extends AbstractConfigurator +{ + private string $suffix; + + public function __construct(Composer $composer, IOInterface $io, Options $options, string $suffix = '') + { + parent::__construct($composer, $io, $options); + $this->suffix = $suffix; + } + + public function configure(Recipe $recipe, $vars, Lock $lock, array $options = []) + { + $this->write('Adding environment variable defaults'.('' === $this->suffix ? '' : ' ('.$this->suffix.')')); + + $this->configureEnvDist($recipe, $vars, $options['force'] ?? false); + + if ('' !== $this->suffix) { + return; + } + + 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'; + $files = '' === $this->suffix ? [$dotenvPath.'.dist', $dotenvPath] : [$dotenvPath.'.'.$this->suffix]; + + foreach ($files 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+)}m', $data.'$1', file_get_contents($phpunit))); + } + } + } + + private function unconfigureEnvFiles(Recipe $recipe, $vars) + { + $dotenvPath = $this->options->get('runtime')['dotenv_path'] ?? '.env'; + $files = '' === $this->suffix ? [$dotenvPath, $dotenvPath.'.dist'] : [$dotenvPath.'.'.$this->suffix]; + + foreach ($files 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', "\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 = '' === $this->suffix ? [$dotenvPath, $dotenvPath.'.dist', 'phpunit.xml.dist', 'phpunit.xml'] : [$dotenvPath.'.'.$this->suffix]; + + 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 ('' === $this->suffix && !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 (!str_starts_with($line, \sprintf('%s=', $var))) { + continue; + } + + return trim(substr($line, \strlen($var) + 1)); + } + + return null; + } +} diff --git a/vendor/symfony/flex/src/Configurator/GitignoreConfigurator.php b/vendor/symfony/flex/src/Configurator/GitignoreConfigurator.php new file mode 100644 index 0000000..9d33d6c --- /dev/null +++ b/vendor/symfony/flex/src/Configurator/GitignoreConfigurator.php @@ -0,0 +1,105 @@ + + * + * 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 + */ +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; + } +} diff --git a/vendor/symfony/flex/src/Configurator/MakefileConfigurator.php b/vendor/symfony/flex/src/Configurator/MakefileConfigurator.php new file mode 100644 index 0000000..5b5abe4 --- /dev/null +++ b/vendor/symfony/flex/src/Configurator/MakefileConfigurator.php @@ -0,0 +1,124 @@ + + * + * 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 + */ +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', + <<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; + } +} diff --git a/vendor/symfony/flex/src/Downloader.php b/vendor/symfony/flex/src/Downloader.php new file mode 100644 index 0000000..02c6221 --- /dev/null +++ b/vendor/symfony/flex/src/Downloader.php @@ -0,0 +1,469 @@ + + * + * 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; +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 + * @author Nicolas Grekas + */ +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; + + private HttpDownloader $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, HttpDownloader $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 Cache($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 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); + } + + $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); + + 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('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 from '.$url.': '.$data['warning'].''); + } + if (!empty($data['info'])) { + $this->io->writeError('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(''.$e->getMessage().''); + $this->io->writeError(''.$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; + } +} diff --git a/vendor/symfony/flex/src/Event/UpdateEvent.php b/vendor/symfony/flex/src/Event/UpdateEvent.php new file mode 100644 index 0000000..06dbe0c --- /dev/null +++ b/vendor/symfony/flex/src/Event/UpdateEvent.php @@ -0,0 +1,38 @@ + + * + * 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; + } +} diff --git a/vendor/symfony/flex/src/Flex.php b/vendor/symfony/flex/src/Flex.php new file mode 100644 index 0000000..77a9a69 --- /dev/null +++ b/vendor/symfony/flex/src/Flex.php @@ -0,0 +1,873 @@ + + * + * 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\Transaction; +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\Locker; +use Composer\Package\Package; +use Composer\Plugin\PluginEvents; +use Composer\Plugin\PluginInterface; +use Composer\Plugin\PrePoolCreateEvent; +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 + * @author Nicolas Grekas + */ +class Flex implements PluginInterface, EventSubscriberInterface +{ + public static $storedOperations = []; + + /** + * @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 $displayThanksReminder = 0; + private $dryRun = false; + private $reinstall; + private static $activated = true; + private static $aliasResolveCommands = [ + 'require' => true, + 'update' => false, + 'remove' => false, + 'unpack' => true, + ]; + private $filter; + + /** + * @return void + */ + public function activate(Composer $composer, IOInterface $io) + { + if (!\extension_loaded('openssl')) { + self::$activated = false; + $io->writeError('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(); + + // if Flex is being upgraded, the original operations from the original Flex + // instance are stored in the static property, so we can reuse them now. + if (property_exists(self::class, 'storedOperations') && self::$storedOperations) { + $this->operations = self::$storedOperations; + self::$storedOperations = []; + } + + $symfonyRequire = preg_replace('/\.x$/', '.x-dev', getenv('SYMFONY_REQUIRE') ?: ($composer->getPackage()->getExtra()['symfony']['require'] ?? '')); + + $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); + } + + $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(); + } + + $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); + + try { + $command = $input->getFirstArgument(); + $command = $command ? $app->find($command)->getName() : null; + } catch (\InvalidArgumentException $e) { + } + + if ('create-project' === $command) { + if ($input->hasOption('remove-vcs')) { + $input->setOption('remove-vcs', true); + } + } elseif ('update' === $command) { + $this->displayThanksReminder = 1; + } elseif ('outdated' === $command) { + $symfonyRequire = null; + } + + if (isset(self::$aliasResolveCommands[$command])) { + if ($input->hasArgument('packages')) { + $input->setArgument('packages', $resolver->resolve($input->getArgument('packages'), self::$aliasResolveCommands[$command])); + } + } + + 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; + } + + $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'))); + $app->add(new Command\DumpEnvCommand($this->config, $this->options)); + + break; + } + } + + /** + * @return void + */ + public function deactivate(Composer $composer, IOInterface $io) + { + // store operations in case Flex is being upgraded + self::$storedOperations = $this->operations; + 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->updateAllowList; + }, $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('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) { + if ('9999999.9999999' === $info['version']) { + // Fix invalid versions found in some lock files + $info['version'] = '99999.9999999'; + } + $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 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 composer recipes at any time to see the status of your Symfony recipes.'); + $this->io->writeError(''); + } + + return; + } + + $this->io->writeError(sprintf('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(' - %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? + [y] Yes + [n] No + [a] Yes for all packages, only for the current installation session + [p] Yes permanently, never ask again for this project + (defaults to 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(' %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; + } + } + + if (method_exists($this->configurator, 'postInstall')) { + foreach ($postInstallRecipes as $recipe) { + $this->configurator->postInstall($recipe, $this->lock, [ + 'force' => $event instanceof UpdateEvent && $event->force(), + ]); + } + } + + if (null !== $manifest) { + array_unshift( + $this->postInstallOutput, + ' ', + ' What\'s next? ', + ' ', + '', + 'Some files have been created and/or updated to configure your new packages.', + 'Please review, edit and commit them: these files are 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, $this->io); + + if ($synchronizer->shouldSynchronize()) { + $lockData = $this->composer->getLocker()->getLockData(); + + if ($synchronizer->synchronize(array_merge($lockData['packages'] ?? [], $lockData['packages-dev'] ?? []))) { + $this->io->writeError('Synchronizing package.json with PHP packages'); + $this->io->writeError('Don\'t forget to run npm install --force or yarn install --force to refresh your JavaScript dependencies!'); + $this->io->writeError(''); + } + } + } + + /** + * @return void + */ + public function uninstall(Composer $composer, IOInterface $io) + { + $this->lock->delete(); + } + + public function enableThanksReminder() + { + if (1 === $this->displayThanksReminder) { + $this->displayThanksReminder = !class_exists(Thanks::class, false) ? 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 = []; + } + + /** + * @return Recipe[] + */ + public function fetchRecipes(array $operations, bool $reset): array + { + if (!$this->downloader->isEnabled()) { + $this->io->writeError('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('%s (>=%s): From %s', $matches[1], $matches[2], 'auto-generated recipe' === $matches[3] ? ''.$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 updateComposerLock() + { + $lock = substr(Factory::getComposerFile(), 0, -4).'lock'; + $composerJson = file_get_contents(Factory::getComposerFile()); + $lockFile = new JsonFile($lock, null, $this->io); + $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('Unpacking Symfony packs'); + foreach ($result->getUnpacked() as $pkg) { + $this->io->writeError(sprintf(' - Unpacked %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 (method_exists($installer, 'setPlatformRequirementFilter')) { + $installer->setPlatformRequirementFilter(((array) $this->installer)["\0*\0platformRequirementFilter"]); + } + + if (!$update) { + $installer->setUpdateAllowList(['php']); + } + + $installer->run(); + + $this->io->write($this->postInstallOutput); + $this->postInstallOutput = []; + } + + public static function getSubscribedEvents(): array + { + if (!self::$activated) { + return []; + } + + $events = [ + PackageEvents::POST_PACKAGE_UPDATE => 'enableThanksReminder', + PackageEvents::POST_PACKAGE_INSTALL => 'recordFlexInstall', + PackageEvents::POST_PACKAGE_UNINSTALL => 'record', + InstallerEvents::PRE_OPERATIONS_EXEC => 'recordOperations', + PluginEvents::PRE_POOL_CREATE => 'truncatePackages', + ScriptEvents::POST_CREATE_PROJECT_CMD => 'configureProject', + ScriptEvents::POST_INSTALL_CMD => 'install', + ScriptEvents::PRE_UPDATE_CMD => 'configureInstaller', + ScriptEvents::POST_UPDATE_CMD => 'update', + 'auto-scripts' => 'executeAutoScripts', + ]; + + 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; + } +} diff --git a/vendor/symfony/flex/src/GithubApi.php b/vendor/symfony/flex/src/GithubApi.php new file mode 100644 index 0000000..391304c --- /dev/null +++ b/vendor/symfony/flex/src/GithubApi.php @@ -0,0 +1,200 @@ + + * + * 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.'+is:pull-request'); + + 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) + { + $contents = $this->downloader->get($path)->getBody(); + + 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]]); + } +} diff --git a/vendor/symfony/flex/src/InformationOperation.php b/vendor/symfony/flex/src/InformationOperation.php new file mode 100644 index 0000000..8cad6a8 --- /dev/null +++ b/vendor/symfony/flex/src/InformationOperation.php @@ -0,0 +1,95 @@ + + */ +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} + * + * @return string + */ + public function getOperationType() + { + return 'information'; + } + + /** + * {@inheritdoc} + * + * @return string + */ + 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; + } +} diff --git a/vendor/symfony/flex/src/Lock.php b/vendor/symfony/flex/src/Lock.php new file mode 100644 index 0000000..a39a15a --- /dev/null +++ b/vendor/symfony/flex/src/Lock.php @@ -0,0 +1,89 @@ + + * + * 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 + */ +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; + } +} diff --git a/vendor/symfony/flex/src/Options.php b/vendor/symfony/flex/src/Options.php new file mode 100644 index 0000000..3e41f22 --- /dev/null +++ b/vendor/symfony/flex/src/Options.php @@ -0,0 +1,88 @@ + + * + * 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 + */ +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; + } +} diff --git a/vendor/symfony/flex/src/PackageFilter.php b/vendor/symfony/flex/src/PackageFilter.php new file mode 100644 index 0000000..091bcd7 --- /dev/null +++ b/vendor/symfony/flex/src/PackageFilter.php @@ -0,0 +1,155 @@ + + * + * 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 + */ +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])) + || ('symfony/psr-http-message-bridge' === $name && 6.4 > $versions[0]) + )) { + $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('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; + } +} diff --git a/vendor/symfony/flex/src/PackageJsonSynchronizer.php b/vendor/symfony/flex/src/PackageJsonSynchronizer.php new file mode 100644 index 0000000..698894c --- /dev/null +++ b/vendor/symfony/flex/src/PackageJsonSynchronizer.php @@ -0,0 +1,403 @@ + + * + * 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\Json\JsonFile; +use Composer\Json\JsonManipulator; +use Composer\Semver\Semver; +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 $io; + private $versionParser; + + public function __construct(string $rootDir, string $vendorDir, ScriptExecutor $scriptExecutor, IOInterface $io) + { + $this->rootDir = $rootDir; + $this->vendorDir = $vendorDir; + $this->scriptExecutor = $scriptExecutor; + $this->io = $io; + $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'] ?? []; + $package = $constraintConfig['package'] ?? $importMapName; + } else { + $constraint = $constraintConfig; + $package = $importMapName; + } + + if (0 === strpos($constraint, 'path:')) { + $path = substr($constraint, 5); + $path = str_replace('%PACKAGE%', \dirname($packageJson->getPath()), $path); + + $dependencies[$importMapName] = [ + 'path' => $path, + ]; + + continue; + } + + $dependencies[$importMapName] = [ + 'version' => $constraint, + 'package' => $package, + ]; + } + + 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 $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])) { + if (!isset($importMapData[$name]['version'])) { + // AssetMapper 6.3 + continue; + } + + $version = $importMapData[$name]['version']; + $versionConstraint = $importMapEntry['version'] ?? null; + + // if the version constraint is satisfied, skip - else, update the package + if (Semver::satisfies($version, $versionConstraint)) { + continue; + } + + $this->io->writeError(sprintf('Updating package %s from %s to %s.', $name, $version, $versionConstraint)); + } + + if (isset($importMapEntry['path'])) { + $arguments = [$name, '--path='.$importMapEntry['path']]; + $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]; + $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; + } + + try { + $previousControllersJson = (new JsonFile($controllersJsonPath))->read(); + } catch (ParsingException $e) { + // if controllers.json is invalid (possible during a recipe upgrade), we can't update the file + return; + } + $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; + } +} diff --git a/vendor/symfony/flex/src/PackageResolver.php b/vendor/symfony/flex/src/PackageResolver.php new file mode 100644 index 0000000..c7a7e5e --- /dev/null +++ b/vendor/symfony/flex/src/PackageResolver.php @@ -0,0 +1,151 @@ + + * + * 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 + */ +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); + } +} diff --git a/vendor/symfony/flex/src/Path.php b/vendor/symfony/flex/src/Path.php new file mode 100644 index 0000000..8c7218b --- /dev/null +++ b/vendor/symfony/flex/src/Path.php @@ -0,0 +1,41 @@ + + * + * 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); + } +} diff --git a/vendor/symfony/flex/src/Recipe.php b/vendor/symfony/flex/src/Recipe.php new file mode 100644 index 0000000..3c86972 --- /dev/null +++ b/vendor/symfony/flex/src/Recipe.php @@ -0,0 +1,123 @@ + + * + * 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 + */ +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('%s (>=%s): From %s', $matches[1], $matches[2], 'auto-generated recipe' === $matches[3] ? ''.$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; + } +} diff --git a/vendor/symfony/flex/src/Response.php b/vendor/symfony/flex/src/Response.php new file mode 100644 index 0000000..f334c03 --- /dev/null +++ b/vendor/symfony/flex/src/Response.php @@ -0,0 +1,90 @@ + + * + * 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 + */ +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; + } + + /** + * @return mixed + */ + #[\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; + } +} diff --git a/vendor/symfony/flex/src/ScriptExecutor.php b/vendor/symfony/flex/src/ScriptExecutor.php new file mode 100644 index 0000000..3162248 --- /dev/null +++ b/vendor/symfony/flex/src/ScriptExecutor.php @@ -0,0 +1,139 @@ + + * + * 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\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 + */ +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 ? ' [OK]' : ' [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(' [KO]'); + $this->io->writeError(sprintf('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', new MatchAllConstraint())) { + $this->io->writeError(sprintf('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 : ''); + } +} diff --git a/vendor/symfony/flex/src/SymfonyBundle.php b/vendor/symfony/flex/src/SymfonyBundle.php new file mode 100644 index 0000000..7d1d8a1 --- /dev/null +++ b/vendor/symfony/flex/src/SymfonyBundle.php @@ -0,0 +1,115 @@ + + * + * 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 + */ +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')); + } +} diff --git a/vendor/symfony/flex/src/Unpack/Operation.php b/vendor/symfony/flex/src/Unpack/Operation.php new file mode 100644 index 0000000..1a6efd3 --- /dev/null +++ b/vendor/symfony/flex/src/Unpack/Operation.php @@ -0,0 +1,49 @@ + + * + * 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; + } +} diff --git a/vendor/symfony/flex/src/Unpack/Result.php b/vendor/symfony/flex/src/Unpack/Result.php new file mode 100644 index 0000000..e352936 --- /dev/null +++ b/vendor/symfony/flex/src/Unpack/Result.php @@ -0,0 +1,55 @@ + + * + * 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']; + } +} diff --git a/vendor/symfony/flex/src/Unpacker.php b/vendor/symfony/flex/src/Unpacker.php new file mode 100644 index 0000000..926a77e --- /dev/null +++ b/vendor/symfony/flex/src/Unpacker.php @@ -0,0 +1,208 @@ + + * + * 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\Factory; +use Composer\IO\IOInterface; +use Composer\Json\JsonFile; +use Composer\Json\JsonManipulator; +use Composer\Package\Locker; +use Composer\Package\Version\VersionSelector; +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 = new RepositorySet($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/ + $locker = new Locker($io, $lockFile, $this->composer->getInstallationManager(), $jsonContent); + $this->composer->setLocker($locker); + } +} diff --git a/vendor/symfony/flex/src/Update/DiffHelper.php b/vendor/symfony/flex/src/Update/DiffHelper.php new file mode 100644 index 0000000..7072889 --- /dev/null +++ b/vendor/symfony/flex/src/Update/DiffHelper.php @@ -0,0 +1,45 @@ + + * + * 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; + } +} diff --git a/vendor/symfony/flex/src/Update/RecipePatch.php b/vendor/symfony/flex/src/Update/RecipePatch.php new file mode 100644 index 0000000..bb6abdb --- /dev/null +++ b/vendor/symfony/flex/src/Update/RecipePatch.php @@ -0,0 +1,52 @@ + + * + * 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; + } +} diff --git a/vendor/symfony/flex/src/Update/RecipePatcher.php b/vendor/symfony/flex/src/Update/RecipePatcher.php new file mode 100644 index 0000000..1de2b34 --- /dev/null +++ b/vendor/symfony/flex/src/Update/RecipePatcher.php @@ -0,0 +1,259 @@ + + * + * 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); + } +} diff --git a/vendor/symfony/flex/src/Update/RecipeUpdate.php b/vendor/symfony/flex/src/Update/RecipeUpdate.php new file mode 100644 index 0000000..9944938 --- /dev/null +++ b/vendor/symfony/flex/src/Update/RecipeUpdate.php @@ -0,0 +1,114 @@ + + * + * 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; + } +}