init
This commit is contained in:
494
backend/vendor/symfony/flex/src/Downloader.php
vendored
Normal file
494
backend/vendor/symfony/flex/src/Downloader.php
vendored
Normal file
@@ -0,0 +1,494 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Flex;
|
||||
|
||||
use Composer\Cache;
|
||||
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\Package\BasePackage;
|
||||
use Composer\Util\Http\Response as ComposerResponse;
|
||||
use Composer\Util\HttpDownloader;
|
||||
use Composer\Util\Loop;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class Downloader
|
||||
{
|
||||
private const DEFAULT_ENDPOINTS = [
|
||||
'https://raw.githubusercontent.com/symfony/recipes/flex/main/index.json',
|
||||
'https://raw.githubusercontent.com/symfony/recipes-contrib/flex/main/index.json',
|
||||
];
|
||||
private const MAX_LENGTH = 1000;
|
||||
|
||||
private static $versions;
|
||||
private static $aliases;
|
||||
|
||||
private $io;
|
||||
private $sess;
|
||||
private $cache;
|
||||
|
||||
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) || str_contains($endpoint, '.json') || 'flex://defaults' === $endpoint) {
|
||||
$this->endpoints = array_values((array) $endpoint);
|
||||
if (\is_string($endpoint) && str_contains($endpoint, '.json')) {
|
||||
$this->endpoints[] = 'flex://defaults';
|
||||
}
|
||||
} else {
|
||||
$this->legacyEndpoint = rtrim($endpoint, '/');
|
||||
}
|
||||
|
||||
if (false === $endpoint = getenv('SYMFONY_ENDPOINT')) {
|
||||
// no-op
|
||||
} elseif (str_contains($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(true);
|
||||
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 (str_starts_with($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]);
|
||||
}
|
||||
|
||||
public function getSymfonyPacks(array $packages)
|
||||
{
|
||||
$packs = [];
|
||||
foreach ($this->composer->getRepositoryManager()->getRepositories() as $repo) {
|
||||
if (!$packages) {
|
||||
break;
|
||||
}
|
||||
|
||||
$result = $repo->loadPackages($packages, BasePackage::$stabilities, []);
|
||||
|
||||
foreach ($result['packages'] ?? [] as $package) {
|
||||
if (!isset($packages[$package->getName()])) {
|
||||
continue;
|
||||
}
|
||||
if ('symfony-pack' === $package->getType()) {
|
||||
$packs[$package->getName()] = true;
|
||||
}
|
||||
unset($packages[$package->getName()]);
|
||||
}
|
||||
}
|
||||
|
||||
return array_keys($packs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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('<warning>Failed to download recipe: '.$e->getMessage().'</>');
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return $responses;
|
||||
}
|
||||
|
||||
private function parseJson(string $json, string $url, string $cacheKey, array $lastHeaders): Response
|
||||
{
|
||||
$data = JsonFile::parseJson($json, $url);
|
||||
if (!empty($data['warning'])) {
|
||||
$this->io->writeError('<warning>Warning from '.$url.': '.$data['warning'].'</>');
|
||||
}
|
||||
if (!empty($data['info'])) {
|
||||
$this->io->writeError('<info>Info from '.$url.': '.$data['info'].'</>');
|
||||
}
|
||||
|
||||
$response = new Response($data, $lastHeaders);
|
||||
if ($cacheKey && ($response->getHeader('last-modified') || $response->getHeader('etag'))) {
|
||||
$this->cache->write($cacheKey, json_encode($response));
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function switchToDegradedMode(\Exception $e, string $url)
|
||||
{
|
||||
if (!$this->degradedMode) {
|
||||
$this->io->writeError('<warning>'.$e->getMessage().'</>');
|
||||
$this->io->writeError('<warning>'.$url.' could not be fully loaded, package information was loaded from the local cache and may be out of date</>');
|
||||
}
|
||||
$this->degradedMode = true;
|
||||
}
|
||||
|
||||
private function getOptions(array $headers): array
|
||||
{
|
||||
$options = ['http' => ['header' => $headers]];
|
||||
|
||||
if (null !== $this->caFile) {
|
||||
$options['ssl']['cafile'] = $this->caFile;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function initialize()
|
||||
{
|
||||
if (null !== $this->index || null === $this->endpoints) {
|
||||
$this->index ?? $this->index = [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$indexes = self::$versions = self::$aliases = [];
|
||||
|
||||
foreach ($this->get(array_keys($this->endpoints)) as $endpoint => $index) {
|
||||
$indexes[$endpoint] = $index;
|
||||
}
|
||||
|
||||
foreach ($this->endpoints as $endpoint => $config) {
|
||||
$config = $indexes[$endpoint] ?? [];
|
||||
foreach ($config['recipes'] ?? [] as $package => $versions) {
|
||||
$this->index[$package] = $this->index[$package] ?? array_fill_keys($versions, $endpoint);
|
||||
}
|
||||
$this->conflicts[] = $config['recipe-conflicts'] ?? [];
|
||||
self::$versions += $config['versions'] ?? [];
|
||||
self::$aliases += $config['aliases'] ?? [];
|
||||
unset($config['recipes'], $config['recipe-conflicts'], $config['versions'], $config['aliases']);
|
||||
$this->endpoints[$endpoint] = $config;
|
||||
}
|
||||
}
|
||||
|
||||
private static function generateCacheKey(string $url): string
|
||||
{
|
||||
$url = preg_replace('{^https://api.github.com/repos/([^/]++/[^/]++)/contents/}', '$1/', $url);
|
||||
$url = preg_replace('{^https://raw.githubusercontent.com/([^/]++/[^/]++)/}', '$1/', $url);
|
||||
|
||||
$key = preg_replace('{[^a-z0-9.]}i', '-', $url);
|
||||
|
||||
// eCryptfs can have problems with filenames longer than around 143 chars
|
||||
return \strlen($key) > 140 ? md5($url) : $key;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user