1
0
Fork 0

Merge pull request #7904 from Seldaek/multi-curl

Parallel downloads
pull/7947/head
Nils Adermann 2019-01-24 15:44:11 +01:00 committed by GitHub
commit 9f18b54cb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
116 changed files with 3398 additions and 1689 deletions

View File

@ -34,7 +34,8 @@
"symfony/console": "^2.7 || ^3.0 || ^4.0",
"symfony/filesystem": "^2.7 || ^3.0 || ^4.0",
"symfony/finder": "^2.7 || ^3.0 || ^4.0",
"symfony/process": "^2.7 || ^3.0 || ^4.0"
"symfony/process": "^2.7 || ^3.0 || ^4.0",
"react/promise": "^1.2 || ^2.7"
},
"conflict": {
"symfony/console": "2.8.38"

46
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "e46280c4cfd37bf3ec8be36095feb20e",
"content-hash": "b078b12b2912d599e0c6904f64def484",
"packages": [
{
"name": "composer/ca-bundle",
@ -342,6 +342,50 @@
],
"time": "2018-11-20T15:27:04+00:00"
},
{
"name": "react/promise",
"version": "v1.2.1",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise.git",
"reference": "eefff597e67ff66b719f8171480add3c91474a1e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/promise/zipball/eefff597e67ff66b719f8171480add3c91474a1e",
"reference": "eefff597e67ff66b719f8171480add3c91474a1e",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
}
},
"autoload": {
"psr-0": {
"React\\Promise": "src/"
},
"files": [
"src/React/Promise/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com"
}
],
"description": "A lightweight implementation of CommonJS Promises/A for PHP",
"time": "2016-03-07T13:46:50+00:00"
},
{
"name": "seld/jsonlint",
"version": "1.7.1",

View File

@ -928,4 +928,9 @@ repository options.
Defaults to `1`. If set to `0`, Composer will not create `.htaccess` files in the
composer home, cache, and data directories.
### COMPOSER_DISABLE_NETWORK
If set to `1`, disables network access (best effort). This can be used for debugging or
to run Composer on a plane or a starship with poor connectivity.
← [Libraries](02-libraries.md) | [Schema](04-schema.md) →

View File

@ -176,8 +176,8 @@ class AwsPlugin implements PluginInterface, EventSubscriberInterface
if ($protocol === 's3') {
$awsClient = new AwsClient($this->io, $this->composer->getConfig());
$s3RemoteFilesystem = new S3RemoteFilesystem($this->io, $event->getRemoteFilesystem()->getOptions(), $awsClient);
$event->setRemoteFilesystem($s3RemoteFilesystem);
$s3Downloader = new S3Downloader($this->io, $event->getHttpDownloader()->getOptions(), $awsClient);
$event->setHttpdownloader($s3Downloader);
}
}
}

View File

@ -61,7 +61,7 @@ Composer fires the following named events during its execution process:
- **command**: occurs before any Composer Command is executed on the CLI. It
provides you with access to the input and output objects of the program.
- **pre-file-download**: occurs before files are downloaded and allows
you to manipulate the `RemoteFilesystem` object prior to downloading files
you to manipulate the `HttpDownloader` object prior to downloading files
based on the URL to be downloaded.
- **pre-command-run**: occurs before a command is executed and allows you to
manipulate the `InputInterface` object's options and arguments to tweak

View File

@ -22,6 +22,7 @@ use Composer\Script\ScriptEvents;
use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents;
use Composer\Util\Filesystem;
use Composer\Util\Loop;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@ -104,8 +105,9 @@ EOT
$archiveManager = $composer->getArchiveManager();
} else {
$factory = new Factory;
$downloadManager = $factory->createDownloadManager($io, $config);
$archiveManager = $factory->createArchiveManager($config, $downloadManager);
$httpDownloader = $factory->createHttpDownloader($io, $config);
$downloadManager = $factory->createDownloadManager($io, $config, $httpDownloader);
$archiveManager = $factory->createArchiveManager($config, $downloadManager, new Loop($httpDownloader));
}
if ($packageName) {

View File

@ -38,6 +38,7 @@ use Symfony\Component\Finder\Finder;
use Composer\Json\JsonFile;
use Composer\Config\JsonConfigSource;
use Composer\Util\Filesystem;
use Composer\Util\Loop;
use Composer\Package\Version\VersionParser;
/**
@ -161,7 +162,6 @@ EOT
}
$composer = Factory::create($io, null, $disablePlugins);
$composer->getDownloadManager()->setOutputProgress(!$noProgress);
$fs = new Filesystem();
@ -345,15 +345,17 @@ EOT
$package = $package->getAliasOf();
}
$dm = $this->createDownloadManager($io, $config);
$factory = new Factory();
$httpDownloader = $factory->createHttpDownloader($io, $config);
$dm = $factory->createDownloadManager($io, $config, $httpDownloader);
$dm->setPreferSource($preferSource)
->setPreferDist($preferDist)
->setOutputProgress(!$noProgress);
->setPreferDist($preferDist);
$projectInstaller = new ProjectInstaller($directory, $dm);
$im = $this->createInstallationManager();
$im = $factory->createInstallationManager(new Loop($httpDownloader));
$im->addInstaller($projectInstaller);
$im->install(new InstalledFilesystemRepository(new JsonFile('php://memory')), new InstallOperation($package));
$im->execute(new InstalledFilesystemRepository(new JsonFile('php://memory')), new InstallOperation($package));
$im->notifyInstalls($io);
// collect suggestions
@ -369,16 +371,4 @@ EOT
return $installedFromVcs;
}
protected function createDownloadManager(IOInterface $io, Config $config)
{
$factory = new Factory();
return $factory->createDownloadManager($io, $config);
}
protected function createInstallationManager()
{
return new InstallationManager();
}
}

View File

@ -22,7 +22,7 @@ use Composer\Plugin\PluginEvents;
use Composer\Util\ConfigValidator;
use Composer\Util\IniHelper;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\Util\StreamContextFactory;
use Composer\SelfUpdate\Keys;
use Composer\SelfUpdate\Versions;
@ -35,8 +35,8 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
class DiagnoseCommand extends BaseCommand
{
/** @var RemoteFilesystem */
protected $rfs;
/** @var HttpDownloader */
protected $httpDownloader;
/** @var ProcessExecutor */
protected $process;
@ -85,7 +85,7 @@ EOT
$config->merge(array('config' => array('secure-http' => false)));
$config->prohibitUrlByConfig('http://repo.packagist.org', new NullIO);
$this->rfs = Factory::createRemoteFilesystem($io, $config);
$this->httpDownloader = Factory::createHttpDownloader($io, $config);
$this->process = new ProcessExecutor($io);
$io->write('Checking platform settings: ', false);
@ -226,7 +226,7 @@ EOT
}
try {
$this->rfs->getContents('packagist.org', $proto . '://repo.packagist.org/packages.json', false);
$this->httpDownloader->get($proto . '://repo.packagist.org/packages.json');
} catch (TransportException $e) {
if (false !== strpos($e->getMessage(), 'cafile')) {
$result[] = '<error>[' . get_class($e) . '] ' . $e->getMessage() . '</error>';
@ -253,11 +253,11 @@ EOT
$protocol = extension_loaded('openssl') ? 'https' : 'http';
try {
$json = json_decode($this->rfs->getContents('packagist.org', $protocol . '://repo.packagist.org/packages.json', false), true);
$json = $this->httpDownloader->get($protocol . '://repo.packagist.org/packages.json')->parseJson();
$hash = reset($json['provider-includes']);
$hash = $hash['sha256'];
$path = str_replace('%hash%', $hash, key($json['provider-includes']));
$provider = $this->rfs->getContents('packagist.org', $protocol . '://repo.packagist.org/'.$path, false);
$provider = $this->httpDownloader->get($protocol . '://repo.packagist.org/'.$path)->getBody();
if (hash('sha256', $provider) !== $hash) {
return 'It seems that your proxy is modifying http traffic on the fly';
@ -285,10 +285,10 @@ EOT
$url = 'http://repo.packagist.org/packages.json';
try {
$this->rfs->getContents('packagist.org', $url, false);
$this->httpDownloader->get($url);
} catch (TransportException $e) {
try {
$this->rfs->getContents('packagist.org', $url, false, array('http' => array('request_fulluri' => false)));
$this->httpDownloader->get($url, array('http' => array('request_fulluri' => false)));
} catch (TransportException $e) {
return 'Unable to assess the situation, maybe packagist.org is down ('.$e->getMessage().')';
}
@ -319,10 +319,10 @@ EOT
$url = 'https://api.github.com/repos/Seldaek/jsonlint/zipball/1.0.0';
try {
$this->rfs->getContents('github.com', $url, false);
$this->httpDownloader->get($url);
} catch (TransportException $e) {
try {
$this->rfs->getContents('github.com', $url, false, array('http' => array('request_fulluri' => false)));
$this->httpDownloader->get($url, array('http' => array('request_fulluri' => false)));
} catch (TransportException $e) {
return 'Unable to assess the situation, maybe github is down ('.$e->getMessage().')';
}
@ -344,7 +344,7 @@ EOT
try {
$url = $domain === 'github.com' ? 'https://api.'.$domain.'/' : 'https://'.$domain.'/api/v3/';
return $this->rfs->getContents($domain, $url, false, array(
return $this->httpDownloader->get($url, array(
'retry-auth-failure' => false,
)) ? true : 'Unexpected error';
} catch (\Exception $e) {
@ -374,8 +374,7 @@ EOT
}
$url = $domain === 'github.com' ? 'https://api.'.$domain.'/rate_limit' : 'https://'.$domain.'/api/rate_limit';
$json = $this->rfs->getContents($domain, $url, false, array('retry-auth-failure' => false));
$data = json_decode($json, true);
$data = $this->httpDownloader->get($url, array('retry-auth-failure' => false))->parseJson();
return $data['resources']['core'];
}
@ -428,7 +427,7 @@ EOT
return $result;
}
$versionsUtil = new Versions($config, $this->rfs);
$versionsUtil = new Versions($config, $this->httpDownloader);
$latest = $versionsUtil->getLatest();
if (Composer::VERSION !== $latest['version'] && Composer::VERSION !== '@package_version@') {

View File

@ -85,7 +85,6 @@ EOT
}
$composer = $this->getComposer(true, $input->getOption('no-plugins'));
$composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'install', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);

View File

@ -126,7 +126,6 @@ EOT
// Update packages
$this->resetComposer();
$composer = $this->getComposer(true, $input->getOption('no-plugins'));
$composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);

View File

@ -167,7 +167,6 @@ EOT
// Update packages
$this->resetComposer();
$composer = $this->getComposer(true, $input->getOption('no-plugins'));
$composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);

View File

@ -76,9 +76,9 @@ EOT
}
$io = $this->getIO();
$remoteFilesystem = Factory::createRemoteFilesystem($io, $config);
$httpDownloader = Factory::createHttpDownloader($io, $config);
$versionsUtil = new Versions($config, $remoteFilesystem);
$versionsUtil = new Versions($config, $httpDownloader);
// switch channel if requested
foreach (array('stable', 'preview', 'snapshot') as $channel) {
@ -155,9 +155,9 @@ EOT
$io->write(sprintf("Updating to version <info>%s</info> (%s channel).", $updateVersion, $versionsUtil->getChannel()));
$remoteFilename = $baseUrl . ($updatingToTag ? "/download/{$updateVersion}/composer.phar" : '/composer.phar');
$signature = $remoteFilesystem->getContents(self::HOMEPAGE, $remoteFilename.'.sig', false);
$signature = $httpDownloader->get($remoteFilename.'.sig')->getBody();
$io->writeError(' ', false);
$remoteFilesystem->copy(self::HOMEPAGE, $remoteFilename, $tempFilename, !$input->getOption('no-progress'));
$httpDownloader->copy($remoteFilename, $tempFilename);
$io->writeError('');
if (!file_exists($tempFilename) || !$signature) {

View File

@ -317,8 +317,8 @@ EOT
} else {
$type = 'available';
}
if ($repo instanceof ComposerRepository && $repo->hasProviders()) {
foreach ($repo->getProviderNames() as $name) {
if ($repo instanceof ComposerRepository) {
foreach ($repo->getPackageNames() as $name) {
if (!$packageFilter || preg_match($packageFilter, $name)) {
$packages[$type][$name] = $name;
}
@ -553,7 +553,7 @@ EOT
$matches[$index] = $package->getId();
}
$pool = $repositorySet->createPool();
$pool = $repositorySet->createPoolForPackage($package->getName());
// select preferred package according to policy rules
if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, array(), $matches)) {

View File

@ -89,7 +89,7 @@ EOT
// list packages
foreach ($installedRepo->getCanonicalPackages() as $package) {
$downloader = $dm->getDownloaderForInstalledPackage($package);
$downloader = $dm->getDownloaderForPackage($package);
$targetDir = $im->getInstallPath($package);
if ($downloader instanceof ChangeReportInterface) {

View File

@ -120,8 +120,6 @@ EOT
}
}
$composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);

View File

@ -123,6 +123,7 @@ class Compiler
->in(__DIR__.'/../../vendor/composer/ca-bundle/')
->in(__DIR__.'/../../vendor/composer/xdebug-handler/')
->in(__DIR__.'/../../vendor/psr/')
->in(__DIR__.'/../../vendor/react/')
->sort($finderSort)
;

View File

@ -32,6 +32,7 @@ class Composer
const VERSION = '@package_version@';
const BRANCH_ALIAS_VERSION = '@package_branch_alias_version@';
const RELEASE_DATE = '@release_date@';
const SOURCE_VERSION = '2.0-source';
/**
* @var Package\RootPackageInterface

View File

@ -217,6 +217,7 @@ class Solver
$this->setupInstalledMap();
$this->io->writeError('Generating rules', true, IOInterface::DEBUG);
$this->ruleSetGenerator = new RuleSetGenerator($this->policy, $this->pool);
$this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap, $ignorePlatformReqs);
$this->checkForRootRequireProblems($ignorePlatformReqs);

View File

@ -30,33 +30,50 @@ abstract class ArchiveDownloader extends FileDownloader
* @throws \RuntimeException
* @throws \UnexpectedValueException
*/
public function download(PackageInterface $package, $path, $output = true)
public function install(PackageInterface $package, $path, $output = true)
{
$temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8);
$retries = 3;
while ($retries--) {
$fileName = parent::download($package, $path, $output);
if ($output) {
$this->io->writeError(" - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)");
}
if ($output) {
$this->io->writeError(' Extracting archive', false, IOInterface::VERBOSE);
$temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8);
$fileName = $this->getFileName($package, $path);
if ($output) {
$this->io->writeError(' Extracting archive', true, IOInterface::VERBOSE);
}
try {
$this->filesystem->ensureDirectoryExists($temporaryDir);
try {
$this->extract($package, $fileName, $temporaryDir);
} catch (\Exception $e) {
// remove cache if the file was corrupted
parent::clearLastCacheWrite($package);
throw $e;
}
try {
$this->filesystem->ensureDirectoryExists($temporaryDir);
try {
$this->extract($fileName, $temporaryDir);
} catch (\Exception $e) {
// remove cache if the file was corrupted
parent::clearLastCacheWrite($package);
throw $e;
$this->filesystem->unlink($fileName);
$renameAsOne = false;
if (!file_exists($path) || ($this->filesystem->isDirEmpty($path) && $this->filesystem->removeDirectory($path))) {
$renameAsOne = true;
}
$contentDir = $this->getFolderContent($temporaryDir);
$singleDirAtTopLevel = 1 === count($contentDir) && is_dir(reset($contentDir));
if ($renameAsOne) {
// if the target $path is clear, we can rename the whole package in one go instead of looping over the contents
if ($singleDirAtTopLevel) {
$extractedDir = (string) reset($contentDir);
} else {
$extractedDir = $temporaryDir;
}
$this->filesystem->unlink($fileName);
$contentDir = $this->getFolderContent($temporaryDir);
$this->filesystem->rename($extractedDir, $path);
} else {
// only one dir in the archive, extract its contents out of it
if (1 === count($contentDir) && is_dir(reset($contentDir))) {
if ($singleDirAtTopLevel) {
$contentDir = $this->getFolderContent((string) reset($contentDir));
}
@ -65,35 +82,24 @@ abstract class ArchiveDownloader extends FileDownloader
$file = (string) $file;
$this->filesystem->rename($file, $path . '/' . basename($file));
}
$this->filesystem->removeDirectory($temporaryDir);
if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir').'/composer/')) {
$this->filesystem->removeDirectory($this->config->get('vendor-dir').'/composer/');
}
if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir'))) {
$this->filesystem->removeDirectory($this->config->get('vendor-dir'));
}
} catch (\Exception $e) {
// clean up
$this->filesystem->removeDirectory($path);
$this->filesystem->removeDirectory($temporaryDir);
// retry downloading if we have an invalid zip file
if ($retries && $e instanceof \UnexpectedValueException && class_exists('ZipArchive') && $e->getCode() === \ZipArchive::ER_NOZIP) {
$this->io->writeError('');
if ($this->io->isDebug()) {
$this->io->writeError(' Invalid zip file ('.$e->getMessage().'), retrying...');
} else {
$this->io->writeError(' Invalid zip file, retrying...');
}
usleep(500000);
continue;
}
throw $e;
}
break;
$this->filesystem->removeDirectory($temporaryDir);
if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir').'/composer/')) {
$this->filesystem->removeDirectory($this->config->get('vendor-dir').'/composer/');
}
if ($this->filesystem->isDirEmpty($this->config->get('vendor-dir'))) {
$this->filesystem->removeDirectory($this->config->get('vendor-dir'));
}
} catch (\Exception $e) {
// clean up
$this->filesystem->removeDirectory($path);
$this->filesystem->removeDirectory($temporaryDir);
if (file_exists($fileName)) {
$this->filesystem->unlink($fileName);
}
throw $e;
}
}
@ -102,7 +108,7 @@ abstract class ArchiveDownloader extends FileDownloader
*/
protected function getFileName(PackageInterface $package, $path)
{
return rtrim($path.'/'.md5($path.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.');
return rtrim($path.'_'.md5($path.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.');
}
/**
@ -113,7 +119,7 @@ abstract class ArchiveDownloader extends FileDownloader
*
* @throws \UnexpectedValueException If can not extract downloaded file to path
*/
abstract protected function extract($file, $path);
abstract protected function extract(PackageInterface $package, $file, $path);
/**
* Returns the folder content, excluding dotfiles

View File

@ -15,6 +15,7 @@ namespace Composer\Downloader;
use Composer\Package\PackageInterface;
use Composer\IO\IOInterface;
use Composer\Util\Filesystem;
use React\Promise\PromiseInterface;
/**
* Downloaders manager.
@ -24,6 +25,7 @@ use Composer\Util\Filesystem;
class DownloadManager
{
private $io;
private $httpDownloader;
private $preferDist = false;
private $preferSource = false;
private $packagePreferences = array();
@ -33,9 +35,9 @@ class DownloadManager
/**
* Initializes download manager.
*
* @param IOInterface $io The Input Output Interface
* @param bool $preferSource prefer downloading from source
* @param Filesystem|null $filesystem custom Filesystem object
* @param IOInterface $io The Input Output Interface
* @param bool $preferSource prefer downloading from source
* @param Filesystem|null $filesystem custom Filesystem object
*/
public function __construct(IOInterface $io, $preferSource = false, Filesystem $filesystem = null)
{
@ -83,22 +85,6 @@ class DownloadManager
return $this;
}
/**
* Sets whether to output download progress information for all registered
* downloaders
*
* @param bool $outputProgress
* @return DownloadManager
*/
public function setOutputProgress($outputProgress)
{
foreach ($this->downloaders as $downloader) {
$downloader->setOutputProgress($outputProgress);
}
return $this;
}
/**
* Sets installer downloader for a specific installation type.
*
@ -140,7 +126,7 @@ class DownloadManager
* wrong type
* @return DownloaderInterface|null
*/
public function getDownloaderForInstalledPackage(PackageInterface $package)
public function getDownloaderForPackage(PackageInterface $package)
{
$installationSource = $package->getInstallationSource();
@ -154,7 +140,7 @@ class DownloadManager
$downloader = $this->getDownloader($package->getSourceType());
} else {
throw new \InvalidArgumentException(
'Package '.$package.' seems not been installed properly'
'Package '.$package.' does not have an installation source set'
);
}
@ -171,63 +157,95 @@ class DownloadManager
return $downloader;
}
public function getDownloaderType(DownloaderInterface $downloader)
{
return array_search($downloader, $this->downloaders);
}
/**
* Downloads package into target dir.
*
* @param PackageInterface $package package instance
* @param string $targetDir target dir
* @param bool $preferSource prefer installation from source
* @param PackageInterface $prevPackage previous package instance in case of updates
*
* @return PromiseInterface
* @throws \InvalidArgumentException if package have no urls to download from
* @throws \RuntimeException
*/
public function download(PackageInterface $package, $targetDir, PackageInterface $prevPackage = null)
{
$this->filesystem->ensureDirectoryExists(dirname($targetDir));
$sources = $this->getAvailableSources($package, $prevPackage);
$io = $this->io;
$self = $this;
$download = function ($retry = false) use (&$sources, $io, $package, $self, $targetDir, &$download) {
$source = array_shift($sources);
if ($retry) {
$io->writeError(' <warning>Now trying to download from ' . $source . '</warning>');
}
$package->setInstallationSource($source);
$downloader = $self->getDownloaderForPackage($package);
if (!$downloader) {
return \React\Promise\resolve();
}
$handleError = function ($e) use ($sources, $source, $package, $io, $download) {
if ($e instanceof \RuntimeException) {
if (!$sources) {
throw $e;
}
$io->writeError(
' <warning>Failed to download '.
$package->getPrettyName().
' from ' . $source . ': '.
$e->getMessage().'</warning>'
);
return $download(true);
}
throw $e;
};
try {
$result = $downloader->download($package, $targetDir);
} catch (\Exception $e) {
return $handleError($e);
}
if (!$result instanceof PromiseInterface) {
return \React\Promise\resolve($result);
}
$res = $result->then(function ($res) {
return $res;
}, $handleError);
return $res;
};
return $download();
}
/**
* Installs package into target dir.
*
* @param PackageInterface $package package instance
* @param string $targetDir target dir
*
* @throws \InvalidArgumentException if package have no urls to download from
* @throws \RuntimeException
*/
public function download(PackageInterface $package, $targetDir, $preferSource = null)
public function install(PackageInterface $package, $targetDir)
{
$preferSource = null !== $preferSource ? $preferSource : $this->preferSource;
$sourceType = $package->getSourceType();
$distType = $package->getDistType();
$sources = array();
if ($sourceType) {
$sources[] = 'source';
}
if ($distType) {
$sources[] = 'dist';
}
if (empty($sources)) {
throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified');
}
if (!$preferSource && ($this->preferDist || 'dist' === $this->resolvePackageInstallPreference($package))) {
$sources = array_reverse($sources);
}
$this->filesystem->ensureDirectoryExists($targetDir);
foreach ($sources as $i => $source) {
if (isset($e)) {
$this->io->writeError(' <warning>Now trying to download from ' . $source . '</warning>');
}
$package->setInstallationSource($source);
try {
$downloader = $this->getDownloaderForInstalledPackage($package);
if ($downloader) {
$downloader->download($package, $targetDir);
}
break;
} catch (\RuntimeException $e) {
if ($i === count($sources) - 1) {
throw $e;
}
$this->io->writeError(
' <warning>Failed to download '.
$package->getPrettyName().
' from ' . $source . ': '.
$e->getMessage().'</warning>'
);
}
$downloader = $this->getDownloaderForPackage($package);
if ($downloader) {
$downloader->install($package, $targetDir);
}
}
@ -242,31 +260,23 @@ class DownloadManager
*/
public function update(PackageInterface $initial, PackageInterface $target, $targetDir)
{
$downloader = $this->getDownloaderForInstalledPackage($initial);
$downloader = $this->getDownloaderForPackage($target);
$initialDownloader = $this->getDownloaderForPackage($initial);
// no downloaders present means update from metapackage to metapackage, nothing to do
if (!$initialDownloader && !$downloader) {
return;
}
// if we have a downloader present before, but not after, the package became a metapackage and its files should be removed
if (!$downloader) {
$initialDownloader->remove($initial, $targetDir);
return;
}
$installationSource = $initial->getInstallationSource();
if ('dist' === $installationSource) {
$initialType = $initial->getDistType();
$targetType = $target->getDistType();
} else {
$initialType = $initial->getSourceType();
$targetType = $target->getSourceType();
}
// upgrading from a dist stable package to a dev package, force source reinstall
if ($target->isDev() && 'dist' === $installationSource) {
$downloader->remove($initial, $targetDir);
$this->download($target, $targetDir);
return;
}
$initialType = $this->getDownloaderType($initialDownloader);
$targetType = $this->getDownloaderType($downloader);
if ($initialType === $targetType) {
$target->setInstallationSource($installationSource);
try {
$downloader->update($initial, $target, $targetDir);
@ -282,8 +292,12 @@ class DownloadManager
}
}
$downloader->remove($initial, $targetDir);
$this->download($target, $targetDir, 'source' === $installationSource);
// if downloader type changed, or update failed and user asks for reinstall,
// we wipe the dir and do a new install instead of updating it
if ($initialDownloader) {
$initialDownloader->remove($initial, $targetDir);
}
$this->install($target, $targetDir);
}
/**
@ -294,7 +308,7 @@ class DownloadManager
*/
public function remove(PackageInterface $package, $targetDir)
{
$downloader = $this->getDownloaderForInstalledPackage($package);
$downloader = $this->getDownloaderForPackage($package);
if ($downloader) {
$downloader->remove($package, $targetDir);
}
@ -322,4 +336,48 @@ class DownloadManager
return $package->isDev() ? 'source' : 'dist';
}
/**
* @return string[]
*/
private function getAvailableSources(PackageInterface $package, PackageInterface $prevPackage = null)
{
$sourceType = $package->getSourceType();
$distType = $package->getDistType();
// add source before dist by default
$sources = array();
if ($sourceType) {
$sources[] = 'source';
}
if ($distType) {
$sources[] = 'dist';
}
if (empty($sources)) {
throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified');
}
if (
$prevPackage
// if we are updating, we want to keep the same source as the previously installed package (if available in the new one)
&& in_array($prevPackage->getInstallationSource(), $sources, true)
// unless the previous package was stable dist (by default) and the new package is dev, then we allow the new default to take over
&& !(!$prevPackage->isDev() && $prevPackage->getInstallationSource() === 'dist' && $package->isDev())
) {
$prevSource = $prevPackage->getInstallationSource();
usort($sources, function ($a, $b) use ($prevSource) {
return $a === $prevSource ? -1 : 1;
});
return $sources;
}
// reverse sources in case dist is the preferred source for this package
if (!$this->preferSource && ($this->preferDist || 'dist' === $this->resolvePackageInstallPreference($package))) {
$sources = array_reverse($sources);
}
return $sources;
}
}

View File

@ -13,6 +13,7 @@
namespace Composer\Downloader;
use Composer\Package\PackageInterface;
use React\Promise\PromiseInterface;
/**
* Downloader interface.
@ -29,13 +30,20 @@ interface DownloaderInterface
*/
public function getInstallationSource();
/**
* This should do any network-related tasks to prepare for install/update
*
* @return PromiseInterface|null
*/
public function download(PackageInterface $package, $path);
/**
* Downloads specific package into specific folder.
*
* @param PackageInterface $package package instance
* @param string $path download path
*/
public function download(PackageInterface $package, $path);
public function install(PackageInterface $package, $path);
/**
* Updates specific package in specific folder from initial to target version.
@ -53,12 +61,4 @@ interface DownloaderInterface
* @param string $path download path
*/
public function remove(PackageInterface $package, $path);
/**
* Sets whether to output download progress information or not
*
* @param bool $outputProgress
* @return DownloaderInterface
*/
public function setOutputProgress($outputProgress);
}

View File

@ -24,8 +24,9 @@ use Composer\Plugin\PluginEvents;
use Composer\Plugin\PreFileDownloadEvent;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\Filesystem;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\Util\Url as UrlUtil;
use Composer\Downloader\TransportException;
/**
* Base downloader for files
@ -39,11 +40,13 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
{
protected $io;
protected $config;
protected $rfs;
protected $httpDownloader;
protected $filesystem;
protected $cache;
protected $outputProgress = true;
private $lastCacheWrites = array();
/**
* @private this is only public for php 5.3 support in closures
*/
public $lastCacheWrites = array();
private $eventDispatcher;
/**
@ -51,17 +54,17 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
*
* @param IOInterface $io The IO instance
* @param Config $config The config
* @param HttpDownloader $httpDownloader The remote filesystem
* @param EventDispatcher $eventDispatcher The event dispatcher
* @param Cache $cache Optional cache instance
* @param RemoteFilesystem $rfs The remote filesystem
* @param Cache $cache Cache instance
* @param Filesystem $filesystem The filesystem
*/
public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, RemoteFilesystem $rfs = null, Filesystem $filesystem = null)
public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $filesystem = null)
{
$this->io = $io;
$this->config = $config;
$this->eventDispatcher = $eventDispatcher;
$this->rfs = $rfs ?: Factory::createRemoteFilesystem($this->io, $config);
$this->httpDownloader = $httpDownloader;
$this->filesystem = $filesystem ?: new Filesystem();
$this->cache = $cache;
@ -87,121 +90,154 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
throw new \InvalidArgumentException('The given package is missing url information');
}
if ($output) {
$this->io->writeError(" - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): ", false);
}
$retries = 3;
$urls = $package->getDistUrls();
while ($url = array_shift($urls)) {
try {
$fileName = $this->doDownload($package, $path, $url);
break;
} catch (\Exception $e) {
if ($this->io->isDebug()) {
$this->io->writeError('');
$this->io->writeError('Failed: ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage());
} elseif (count($urls)) {
$this->io->writeError('');
$this->io->writeError(' Failed, trying the next URL ('.$e->getCode().': '.$e->getMessage().')', false);
}
if (!count($urls)) {
throw $e;
}
}
foreach ($urls as $index => $url) {
$processedUrl = $this->processUrl($package, $url);
$urls[$index] = array(
'base' => $url,
'processed' => $processedUrl,
'cacheKey' => $this->getCacheKey($package, $processedUrl)
);
}
if ($output) {
$this->io->writeError('');
}
return $fileName;
}
protected function doDownload(PackageInterface $package, $path, $url)
{
$this->filesystem->emptyDirectory($path);
$fileName = $this->getFileName($package, $path);
$processedUrl = $this->processUrl($package, $url);
$hostname = parse_url($processedUrl, PHP_URL_HOST);
$io = $this->io;
$cache = $this->cache;
$httpDownloader = $this->httpDownloader;
$eventDispatcher = $this->eventDispatcher;
$filesystem = $this->filesystem;
$self = $this;
$preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->rfs, $processedUrl);
if ($this->eventDispatcher) {
$this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
}
$rfs = $preFileDownloadEvent->getRemoteFilesystem();
$accept = null;
$reject = null;
$download = function () use ($io, $output, $httpDownloader, $cache, $eventDispatcher, $package, $fileName, $path, &$urls, &$accept, &$reject) {
$url = reset($urls);
if ($eventDispatcher) {
$preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed']);
$eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
}
try {
$checksum = $package->getDistSha1Checksum();
$cacheKey = $this->getCacheKey($package, $processedUrl);
$cacheKey = $url['cacheKey'];
// use from cache if it is present and has a valid checksum or we have no checksum to check against
if ($this->cache && (!$checksum || $checksum === $this->cache->sha1($cacheKey)) && $this->cache->copyTo($cacheKey, $fileName)) {
$this->io->writeError('Loading from cache', false);
if ($cache && (!$checksum || $checksum === $cache->sha1($cacheKey)) && $cache->copyTo($cacheKey, $fileName)) {
if ($output) {
$io->writeError(" - Loading <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>) from cache");
}
$result = \React\Promise\resolve($fileName);
} else {
// download if cache restore failed
if (!$this->outputProgress) {
$this->io->writeError('Downloading', false);
if ($output) {
$io->writeError(" - Downloading <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)");
}
// try to download 3 times then fail hard
$retries = 3;
while ($retries--) {
try {
$rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress, $package->getTransportOptions());
break;
} catch (TransportException $e) {
// if we got an http response with a proper code, then requesting again will probably not help, abort
if ((0 !== $e->getCode() && !in_array($e->getCode(), array(500, 502, 503, 504))) || !$retries) {
throw $e;
}
$this->io->writeError('');
$this->io->writeError(' Download failed, retrying...', true, IOInterface::VERBOSE);
usleep(500000);
}
}
if (!$this->outputProgress) {
$this->io->writeError(' (<comment>100%</comment>)', false);
}
if ($this->cache) {
$this->lastCacheWrites[$package->getName()] = $cacheKey;
$this->cache->copyFrom($cacheKey, $fileName);
}
$result = $httpDownloader->addCopy($url['processed'], $fileName, $package->getTransportOptions())
->then($accept, $reject);
}
if (!file_exists($fileName)) {
throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the'
.' directory is writable and you have internet connectivity');
return $result->then(function ($result) use ($fileName, $checksum, $url) {
// in case of retry, the first call's Promise chain finally calls this twice at the end,
// once with $result being the returned $fileName from $accept, and then once for every
// failed request with a null result, which can be skipped.
if (null === $result) {
return $fileName;
}
if (!file_exists($fileName)) {
throw new \UnexpectedValueException($url['base'].' could not be saved to '.$fileName.', make sure the'
.' directory is writable and you have internet connectivity');
}
if ($checksum && hash_file('sha1', $fileName) !== $checksum) {
throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url['base'].')');
}
return $fileName;
});
};
$accept = function ($response) use ($io, $cache, $package, $fileName, $path, $self, &$urls) {
$url = reset($urls);
$cacheKey = $url['cacheKey'];
if ($cache) {
$self->lastCacheWrites[$package->getName()] = $cacheKey;
$cache->copyFrom($cacheKey, $fileName);
}
if ($checksum && hash_file('sha1', $fileName) !== $checksum) {
throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url.')');
}
} catch (\Exception $e) {
$response->collect();
return $fileName;
};
$reject = function ($e) use ($io, &$urls, $download, $fileName, $path, $package, &$retries, $filesystem, $self) {
// clean up
$this->filesystem->removeDirectory($path);
$this->clearLastCacheWrite($package);
throw $e;
}
$filesystem->removeDirectory($path);
$self->clearLastCacheWrite($package);
return $fileName;
if ($e instanceof TransportException) {
// if we got an http response with a proper code, then requesting again will probably not help, abort
if ((0 !== $e->getCode() && !in_array($e->getCode(), array(500, 502, 503, 504))) || !$retries) {
$retries = 0;
}
}
// special error code returned when network is being artificially disabled
if ($e instanceof TransportException && $e->getStatusCode() === 499) {
$retries = 0;
$urls = array();
}
if ($retries) {
usleep(500000);
$retries--;
return $download();
}
array_shift($urls);
if ($urls) {
if ($io->isDebug()) {
$io->writeError(' Failed downloading '.$package->getName().': ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage());
$io->writeError(' Trying the next URL for '.$package->getName());
} elseif (count($urls)) {
$io->writeError(' Failed downloading '.$package->getName().', trying the next URL ('.$e->getCode().': '.$e->getMessage().')');
}
$retries = 3;
usleep(100000);
return $download();
}
throw $e;
};
return $download();
}
/**
* {@inheritDoc}
*/
public function setOutputProgress($outputProgress)
public function install(PackageInterface $package, $path, $output = true)
{
$this->outputProgress = $outputProgress;
if ($output) {
$this->io->writeError(" - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)");
}
return $this;
$this->filesystem->ensureDirectoryExists($path);
$this->filesystem->rename($this->getFileName($package, $path), $path . pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME));
}
protected function clearLastCacheWrite(PackageInterface $package)
/**
* TODO mark private in v3
* @protected This is public due to PHP 5.3
*/
public function clearLastCacheWrite(PackageInterface $package)
{
if ($this->cache && isset($this->lastCacheWrites[$package->getName()])) {
$this->cache->remove($this->lastCacheWrites[$package->getName()]);
@ -222,7 +258,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
$this->io->writeError(" - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>): ", false);
$this->remove($initial, $path, false);
$this->download($target, $path, false);
$this->install($target, $path, false);
$this->io->writeError('');
}
@ -249,7 +285,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
*/
protected function getFileName(PackageInterface $package, $path)
{
return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
return $path.'_'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
}
/**
@ -291,15 +327,15 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
public function getLocalChanges(PackageInterface $package, $targetDir)
{
$prevIO = $this->io;
$prevProgress = $this->outputProgress;
$this->io = new NullIO;
$this->io->loadConfiguration($this->config);
$this->outputProgress = false;
$e = null;
try {
$this->download($package, $targetDir.'_compare', false);
$res = $this->download($package, $targetDir.'_compare', false);
$this->httpDownloader->wait();
$res = $this->install($package, $targetDir.'_compare', false);
$comparer = new Comparer();
$comparer->setSource($targetDir.'_compare');
@ -311,7 +347,6 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
}
$this->io = $prevIO;
$this->outputProgress = $prevProgress;
if ($e) {
throw $e;

View File

@ -23,7 +23,7 @@ class FossilDownloader extends VcsDownloader
/**
* {@inheritDoc}
*/
public function doDownload(PackageInterface $package, $path, $url)
public function doInstall(PackageInterface $package, $path, $url)
{
// Ensure we are allowed to use this URL by config
$this->config->prohibitUrlByConfig($url, $this->io);

View File

@ -38,7 +38,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface
/**
* {@inheritDoc}
*/
public function doDownload(PackageInterface $package, $path, $url)
public function doInstall(PackageInterface $package, $path, $url)
{
GitUtil::cleanEnv();
$path = $this->normalizePath($path);

View File

@ -18,7 +18,7 @@ use Composer\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface;
use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface;
/**
@ -30,15 +30,16 @@ class GzipDownloader extends ArchiveDownloader
{
protected $process;
public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null)
public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null)
{
$this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
}
protected function extract($file, $path)
protected function extract(PackageInterface $package, $file, $path)
{
$targetFilepath = $path . DIRECTORY_SEPARATOR . basename(substr($file, 0, -3));
$filename = pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_FILENAME);
$targetFilepath = $path . DIRECTORY_SEPARATOR . $filename;
// Try to use gunzip on *nix
if (!Platform::isWindows()) {
@ -63,14 +64,6 @@ class GzipDownloader extends ArchiveDownloader
$this->extractUsingExt($file, $targetFilepath);
}
/**
* {@inheritdoc}
*/
protected function getFileName(PackageInterface $package, $path)
{
return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
}
private function extractUsingExt($file, $targetFilepath)
{
$archiveFile = gzopen($file, 'rb');

View File

@ -24,7 +24,7 @@ class HgDownloader extends VcsDownloader
/**
* {@inheritDoc}
*/
public function doDownload(PackageInterface $package, $path, $url)
public function doInstall(PackageInterface $package, $path, $url)
{
$hgUtils = new HgUtils($this->io, $this->config, $this->process);

View File

@ -61,6 +61,15 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter
$realUrl
));
}
}
/**
* {@inheritdoc}
*/
public function install(PackageInterface $package, $path, $output = true)
{
$url = $package->getDistUrl();
$realUrl = realpath($url);
// Get the transport options with default values
$transportOptions = $package->getTransportOptions() + array('symlink' => null);

View File

@ -27,7 +27,7 @@ class PerforceDownloader extends VcsDownloader
/**
* {@inheritDoc}
*/
public function doDownload(PackageInterface $package, $path, $url)
public function doInstall(PackageInterface $package, $path, $url)
{
$ref = $package->getSourceReference();
$label = $this->getLabelFromSourceReference($ref);

View File

@ -12,6 +12,8 @@
namespace Composer\Downloader;
use Composer\Package\PackageInterface;
/**
* Downloader for phar files
*
@ -22,7 +24,7 @@ class PharDownloader extends ArchiveDownloader
/**
* {@inheritDoc}
*/
protected function extract($file, $path)
protected function extract(PackageInterface $package, $file, $path)
{
// Can throw an UnexpectedValueException
$archive = new \Phar($file);

View File

@ -18,8 +18,9 @@ use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\IniHelper;
use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface;
use Composer\Package\PackageInterface;
use RarArchive;
/**
@ -33,13 +34,13 @@ class RarDownloader extends ArchiveDownloader
{
protected $process;
public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null)
public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null)
{
$this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
}
protected function extract($file, $path)
protected function extract(PackageInterface $package, $file, $path)
{
$processError = null;

View File

@ -28,7 +28,7 @@ class SvnDownloader extends VcsDownloader
/**
* {@inheritDoc}
*/
public function doDownload(PackageInterface $package, $path, $url)
public function doInstall(PackageInterface $package, $path, $url)
{
SvnUtil::cleanEnv();
$ref = $package->getSourceReference();

View File

@ -12,6 +12,8 @@
namespace Composer\Downloader;
use Composer\Package\PackageInterface;
/**
* Downloader for tar files: tar, tar.gz or tar.bz2
*
@ -22,7 +24,7 @@ class TarDownloader extends ArchiveDownloader
/**
* {@inheritDoc}
*/
protected function extract($file, $path)
protected function extract(PackageInterface $package, $file, $path)
{
// Can throw an UnexpectedValueException
$archive = new \PharData($file);

View File

@ -55,6 +55,14 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
* {@inheritDoc}
*/
public function download(PackageInterface $package, $path)
{
// noop for now, ideally we would do a git fetch already here, or make sure the cached git repo is synced, etc.
}
/**
* {@inheritDoc}
*/
public function install(PackageInterface $package, $path)
{
if (!$package->getSourceReference()) {
throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information');
@ -87,7 +95,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
$url = $needle . $url;
}
}
$this->doDownload($package, $path, $url);
$this->doInstall($package, $path, $url);
break;
} catch (\Exception $e) {
// rethrow phpunit exceptions to avoid hard to debug bug failures
@ -202,15 +210,6 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
}
}
/**
* Download progress information is not available for all VCS downloaders.
* {@inheritDoc}
*/
public function setOutputProgress($outputProgress)
{
return $this;
}
/**
* {@inheritDoc}
*/
@ -260,7 +259,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
* @param string $path download path
* @param string $url package url
*/
abstract protected function doDownload(PackageInterface $package, $path, $url);
abstract protected function doInstall(PackageInterface $package, $path, $url);
/**
* Updates specific package in specific folder from initial to target version.

View File

@ -17,7 +17,7 @@ use Composer\Cache;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface;
/**
@ -30,14 +30,14 @@ class XzDownloader extends ArchiveDownloader
{
protected $process;
public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null)
public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null)
{
$this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
}
protected function extract($file, $path)
protected function extract(PackageInterface $package, $file, $path)
{
$command = 'tar -xJf ' . ProcessExecutor::escape($file) . ' -C ' . ProcessExecutor::escape($path);
@ -49,12 +49,4 @@ class XzDownloader extends ArchiveDownloader
throw new \RuntimeException($processError);
}
/**
* {@inheritdoc}
*/
protected function getFileName(PackageInterface $package, $path)
{
return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
}
}

View File

@ -19,7 +19,7 @@ use Composer\Package\PackageInterface;
use Composer\Util\IniHelper;
use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface;
use Symfony\Component\Process\ExecutableFinder;
use ZipArchive;
@ -36,10 +36,10 @@ class ZipDownloader extends ArchiveDownloader
protected $process;
private $zipArchiveObject;
public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null)
public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null)
{
$this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
parent::__construct($io, $config, $downloader, $eventDispatcher, $cache);
}
/**
@ -185,7 +185,7 @@ class ZipDownloader extends ArchiveDownloader
* @param string $file File to extract
* @param string $path Path where to extract file
*/
public function extract($file, $path)
public function extract(PackageInterface $package, $file, $path)
{
// Each extract calls its alternative if not available or fails
if (self::$isWindows) {

View File

@ -23,7 +23,8 @@ use Composer\Repository\WritableRepositoryInterface;
use Composer\Util\Filesystem;
use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\Util\Loop;
use Composer\Util\Silencer;
use Composer\Plugin\PluginEvents;
use Composer\EventDispatcher\Event;
@ -325,14 +326,15 @@ class Factory
$io->loadConfiguration($config);
}
$rfs = self::createRemoteFilesystem($io, $config);
$httpDownloader = self::createHttpDownloader($io, $config);
$loop = new Loop($httpDownloader);
// initialize event dispatcher
$dispatcher = new EventDispatcher($composer, $io);
$composer->setEventDispatcher($dispatcher);
// initialize repository manager
$rm = RepositoryFactory::manager($io, $config, $dispatcher, $rfs);
$rm = RepositoryFactory::manager($io, $config, $httpDownloader, $dispatcher);
$composer->setRepositoryManager($rm);
// load local repository
@ -352,12 +354,12 @@ class Factory
$composer->setPackage($package);
// initialize installation manager
$im = $this->createInstallationManager();
$im = $this->createInstallationManager($loop);
$composer->setInstallationManager($im);
if ($fullLoad) {
// initialize download manager
$dm = $this->createDownloadManager($io, $config, $dispatcher, $rfs);
$dm = $this->createDownloadManager($io, $config, $httpDownloader, $dispatcher);
$composer->setDownloadManager($dm);
// initialize autoload generator
@ -365,7 +367,7 @@ class Factory
$composer->setAutoloadGenerator($generator);
// initialize archive manager
$am = $this->createArchiveManager($config, $dm);
$am = $this->createArchiveManager($config, $dm, $loop);
$composer->setArchiveManager($am);
}
@ -451,7 +453,7 @@ class Factory
* @param EventDispatcher $eventDispatcher
* @return Downloader\DownloadManager
*/
public function createDownloadManager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null)
public function createDownloadManager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null)
{
$cache = null;
if ($config->get('cache-files-ttl') > 0) {
@ -484,14 +486,14 @@ class Factory
$dm->setDownloader('fossil', new Downloader\FossilDownloader($io, $config, $executor, $fs));
$dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $executor, $fs));
$dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config));
$dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
$dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
$dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $eventDispatcher, $cache, $rfs));
$dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
$dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
$dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $eventDispatcher, $cache, $rfs));
$dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $eventDispatcher, $cache, $rfs));
$dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $eventDispatcher, $cache, $rfs));
$dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
$dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
$dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
$dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
$dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
$dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
$dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
$dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
return $dm;
}
@ -501,15 +503,9 @@ class Factory
* @param Downloader\DownloadManager $dm Manager use to download sources
* @return Archiver\ArchiveManager
*/
public function createArchiveManager(Config $config, Downloader\DownloadManager $dm = null)
public function createArchiveManager(Config $config, Downloader\DownloadManager $dm, Loop $loop)
{
if (null === $dm) {
$io = new IO\NullIO();
$io->loadConfiguration($config);
$dm = $this->createDownloadManager($io, $config);
}
$am = new Archiver\ArchiveManager($dm);
$am = new Archiver\ArchiveManager($dm, $loop);
$am->addArchiver(new Archiver\ZipArchiver);
$am->addArchiver(new Archiver\PharArchiver);
@ -531,9 +527,9 @@ class Factory
/**
* @return Installer\InstallationManager
*/
protected function createInstallationManager()
public function createInstallationManager(Loop $loop)
{
return new Installer\InstallationManager();
return new Installer\InstallationManager($loop);
}
/**
@ -579,10 +575,10 @@ class Factory
/**
* @param IOInterface $io IO instance
* @param Config $config Config instance
* @param array $options Array of options passed directly to RemoteFilesystem constructor
* @return RemoteFilesystem
* @param array $options Array of options passed directly to HttpDownloader constructor
* @return HttpDownloader
*/
public static function createRemoteFilesystem(IOInterface $io, Config $config = null, $options = array())
public static function createHttpDownloader(IOInterface $io, Config $config = null, $options = array())
{
static $warned = false;
$disableTls = false;
@ -596,18 +592,18 @@ class Factory
throw new Exception\NoSslException('The openssl extension is required for SSL/TLS protection but is not available. '
. 'If you can not enable the openssl extension, you can disable this error, at your own risk, by setting the \'disable-tls\' option to true.');
}
$remoteFilesystemOptions = array();
$httpDownloaderOptions = array();
if ($disableTls === false) {
if ($config && $config->get('cafile')) {
$remoteFilesystemOptions['ssl']['cafile'] = $config->get('cafile');
$httpDownloaderOptions['ssl']['cafile'] = $config->get('cafile');
}
if ($config && $config->get('capath')) {
$remoteFilesystemOptions['ssl']['capath'] = $config->get('capath');
$httpDownloaderOptions['ssl']['capath'] = $config->get('capath');
}
$remoteFilesystemOptions = array_replace_recursive($remoteFilesystemOptions, $options);
$httpDownloaderOptions = array_replace_recursive($httpDownloaderOptions, $options);
}
try {
$remoteFilesystem = new RemoteFilesystem($io, $config, $remoteFilesystemOptions, $disableTls);
$httpDownloader = new HttpDownloader($io, $config, $httpDownloaderOptions, $disableTls);
} catch (TransportException $e) {
if (false !== strpos($e->getMessage(), 'cafile')) {
$io->write('<error>Unable to locate a valid CA certificate file. You must set a valid \'cafile\' option.</error>');
@ -620,7 +616,7 @@ class Factory
throw $e;
}
return $remoteFilesystem;
return $httpDownloader;
}
/**

View File

@ -24,6 +24,7 @@ use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation;
use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation;
use Composer\Util\StreamContextFactory;
use Composer\Util\Loop;
/**
* Package operation manager.
@ -37,6 +38,12 @@ class InstallationManager
private $installers = array();
private $cache = array();
private $notifiablePackages = array();
private $loop;
public function __construct(Loop $loop)
{
$this->loop = $loop;
}
public function reset()
{
@ -156,7 +163,24 @@ class InstallationManager
*/
public function execute(RepositoryInterface $repo, OperationInterface $operation)
{
// TODO this should take all operations in one go
$method = $operation->getJobType();
if ($method === 'install') {
$package = $operation->getPackage();
$installer = $this->getInstaller($package->getType());
$promise = $installer->download($package);
} elseif ($method === 'update') {
$target = $operation->getTargetPackage();
$targetType = $target->getType();
$installer = $this->getInstaller($targetType);
$promise = $installer->download($target, $operation->getInitialPackage());
}
if (isset($promise)) {
$this->loop->wait(array($promise));
}
$this->$method($repo, $operation);
}
@ -194,7 +218,8 @@ class InstallationManager
$this->markForNotification($target);
} else {
$this->getInstaller($initialType)->uninstall($repo, $initial);
$this->getInstaller($targetType)->install($repo, $target);
$installer = $this->getInstaller($targetType);
$installer->install($repo, $target);
}
}

View File

@ -15,6 +15,7 @@ namespace Composer\Installer;
use Composer\Package\PackageInterface;
use Composer\Repository\InstalledRepositoryInterface;
use InvalidArgumentException;
use React\Promise\PromiseInterface;
/**
* Interface for the package installation manager.
@ -42,6 +43,15 @@ interface InstallerInterface
*/
public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package);
/**
* Downloads the files needed to later install the given package.
*
* @param PackageInterface $package package instance
* @param PackageInterface $prevPackage previous package instance in case of an update
* @return PromiseInterface
*/
public function download(PackageInterface $package, PackageInterface $prevPackage = null);
/**
* Installs specific package.
*

View File

@ -85,6 +85,14 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
return (Platform::isWindows() && $this->filesystem->isJunction($installPath)) || is_link($installPath);
}
public function download(PackageInterface $package, PackageInterface $prevPackage = null)
{
$this->initializeVendorDir();
$downloadPath = $this->getInstallPath($package);
return $this->downloadManager->download($package, $downloadPath, $prevPackage);
}
/**
* {@inheritDoc}
*/
@ -194,7 +202,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface
protected function installCode(PackageInterface $package)
{
$downloadPath = $this->getInstallPath($package);
$this->downloadManager->download($package, $downloadPath);
$this->downloadManager->install($package, $downloadPath);
}
protected function updateCode(PackageInterface $initial, PackageInterface $target)

View File

@ -38,6 +38,14 @@ class MetapackageInstaller implements InstallerInterface
return $repo->hasPackage($package);
}
/**
* {@inheritDoc}
*/
public function download(PackageInterface $package, PackageInterface $prevPackage = null)
{
// noop
}
/**
* {@inheritDoc}
*/

View File

@ -40,6 +40,13 @@ class NoopInstaller implements InstallerInterface
return $repo->hasPackage($package);
}
/**
* {@inheritDoc}
*/
public function download(PackageInterface $package, PackageInterface $prevPackage = null)
{
}
/**
* {@inheritDoc}
*/

View File

@ -50,13 +50,21 @@ class PluginInstaller extends LibraryInstaller
/**
* {@inheritDoc}
*/
public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
public function download(PackageInterface $package, PackageInterface $prevPackage = null)
{
$extra = $package->getExtra();
if (empty($extra['class'])) {
throw new \UnexpectedValueException('Error while installing '.$package->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.');
}
return parent::download($package, $prevPackage);
}
/**
* {@inheritDoc}
*/
public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
{
parent::install($repo, $package);
try {
$this->composer->getPluginManager()->registerPackage($package, true);

View File

@ -58,7 +58,7 @@ class ProjectInstaller implements InstallerInterface
/**
* {@inheritDoc}
*/
public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
public function download(PackageInterface $package, PackageInterface $prevPackage = null)
{
$installPath = $this->installPath;
if (file_exists($installPath) && !$this->filesystem->isDirEmpty($installPath)) {
@ -67,7 +67,16 @@ class ProjectInstaller implements InstallerInterface
if (!is_dir($installPath)) {
mkdir($installPath, 0777, true);
}
$this->downloadManager->download($package, $installPath);
return $this->downloadManager->download($package, $installPath, $prevPackage);
}
/**
* {@inheritDoc}
*/
public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
{
$this->downloadManager->install($package, $this->installPath);
}
/**

View File

@ -15,7 +15,7 @@ namespace Composer\Json;
use JsonSchema\Validator;
use Seld\JsonLint\JsonParser;
use Seld\JsonLint\ParsingException;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface;
use Composer\Downloader\TransportException;
@ -35,25 +35,25 @@ class JsonFile
const JSON_UNESCAPED_UNICODE = 256;
private $path;
private $rfs;
private $httpDownloader;
private $io;
/**
* Initializes json file reader/parser.
*
* @param string $path path to a lockfile
* @param RemoteFilesystem $rfs required for loading http/https json files
* @param string $path path to a lockfile
* @param HttpDownloader $httpDownloader required for loading http/https json files
* @param IOInterface $io
* @throws \InvalidArgumentException
*/
public function __construct($path, RemoteFilesystem $rfs = null, IOInterface $io = null)
public function __construct($path, HttpDownloader $httpDownloader = null, IOInterface $io = null)
{
$this->path = $path;
if (null === $rfs && preg_match('{^https?://}i', $path)) {
throw new \InvalidArgumentException('http urls require a RemoteFilesystem instance to be passed');
if (null === $httpDownloader && preg_match('{^https?://}i', $path)) {
throw new \InvalidArgumentException('http urls require a HttpDownloader instance to be passed');
}
$this->rfs = $rfs;
$this->httpDownloader = $httpDownloader;
$this->io = $io;
}
@ -84,8 +84,8 @@ class JsonFile
public function read()
{
try {
if ($this->rfs) {
$json = $this->rfs->getContents($this->path, $this->path, false);
if ($this->httpDownloader) {
$json = $this->httpDownloader->get($this->path)->getBody();
} else {
if ($this->io && $this->io->isDebug()) {
$this->io->writeError('Reading ' . $this->path);

View File

@ -16,6 +16,7 @@ use Composer\Downloader\DownloadManager;
use Composer\Package\PackageInterface;
use Composer\Package\RootPackageInterface;
use Composer\Util\Filesystem;
use Composer\Util\Loop;
use Composer\Json\JsonFile;
/**
@ -25,6 +26,7 @@ use Composer\Json\JsonFile;
class ArchiveManager
{
protected $downloadManager;
protected $loop;
protected $archivers = array();
@ -36,9 +38,10 @@ class ArchiveManager
/**
* @param DownloadManager $downloadManager A manager used to download package sources
*/
public function __construct(DownloadManager $downloadManager)
public function __construct(DownloadManager $downloadManager, Loop $loop)
{
$this->downloadManager = $downloadManager;
$this->loop = $loop;
}
/**
@ -148,7 +151,9 @@ class ArchiveManager
$filesystem->ensureDirectoryExists($sourcePath);
// Download sources
$this->downloadManager->download($package, $sourcePath);
$promise = $this->downloadManager->download($package, $sourcePath);
$this->loop->wait(array($promise));
$this->downloadManager->install($package, $sourcePath);
// Check exclude from downloaded composer.json
if (file_exists($composerJsonPath = $sourcePath.'/composer.json')) {

View File

@ -18,7 +18,6 @@ use Composer\Package\Link;
use Composer\Package\RootAliasPackage;
use Composer\Package\RootPackageInterface;
use Composer\Package\Version\VersionParser;
use Composer\Semver\VersionParser as SemverVersionParser;
/**
* @author Konstantin Kudryashiv <ever.zet@gmail.com>
@ -29,7 +28,7 @@ class ArrayLoader implements LoaderInterface
protected $versionParser;
protected $loadOptions;
public function __construct(SemverVersionParser $parser = null, $loadOptions = false)
public function __construct(VersionParser $parser = null, $loadOptions = false)
{
if (!$parser) {
$parser = new VersionParser;
@ -39,6 +38,69 @@ class ArrayLoader implements LoaderInterface
}
public function load(array $config, $class = 'Composer\Package\CompletePackage')
{
$package = $this->createObject($config, $class);
foreach (Package\BasePackage::$supportedLinkTypes as $type => $opts) {
if (isset($config[$type])) {
$method = 'set'.ucfirst($opts['method']);
$package->{$method}(
$this->parseLinks(
$package->getName(),
$package->getPrettyVersion(),
$opts['description'],
$config[$type]
)
);
}
}
$package = $this->configureObject($package, $config);
return $package;
}
public function loadPackages(array $versions, $class)
{
static $uniqKeys = array('version', 'version_normalized', 'source', 'dist', 'time');
$packages = array();
$linkCache = array();
foreach ($versions as $version) {
if (isset($version['versions'])) {
$baseVersion = $version;
foreach ($uniqKeys as $key) {
unset($baseVersion[$key.'s']);
}
foreach ($version['versions'] as $index => $dummy) {
$unpackedVersion = $baseVersion;
foreach ($uniqKeys as $key) {
$unpackedVersion[$key] = $version[$key.'s'][$index];
}
$package = $this->createObject($unpackedVersion, $class);
$this->configureCachedLinks($linkCache, $package, $unpackedVersion);
$package = $this->configureObject($package, $unpackedVersion);
$packages[] = $package;
}
} else {
$package = $this->createObject($version, $class);
$this->configureCachedLinks($linkCache, $package, $version);
$package = $this->configureObject($package, $version);
$packages[] = $package;
}
}
return $packages;
}
private function createObject(array $config, $class)
{
if (!isset($config['name'])) {
throw new \UnexpectedValueException('Unknown package has no name defined ('.json_encode($config).').');
@ -53,7 +115,12 @@ class ArrayLoader implements LoaderInterface
} else {
$version = $this->versionParser->normalize($config['version']);
}
$package = new $class($config['name'], $version, $config['version']);
return new $class($config['name'], $version, $config['version']);
}
private function configureObject($package, array $config)
{
$package->setType(isset($config['type']) ? strtolower($config['type']) : 'library');
if (isset($config['target-dir'])) {
@ -110,20 +177,6 @@ class ArrayLoader implements LoaderInterface
}
}
foreach (Package\BasePackage::$supportedLinkTypes as $type => $opts) {
if (isset($config[$type])) {
$method = 'set'.ucfirst($opts['method']);
$package->{$method}(
$this->parseLinks(
$package->getName(),
$package->getPrettyVersion(),
$opts['description'],
$config[$type]
)
);
}
}
if (isset($config['suggest']) && is_array($config['suggest'])) {
foreach ($config['suggest'] as $target => $reason) {
if ('self.version' === trim($reason)) {
@ -203,21 +256,50 @@ class ArrayLoader implements LoaderInterface
}
}
if ($aliasNormalized = $this->getBranchAlias($config)) {
if ($package instanceof RootPackageInterface) {
$package = new RootAliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized));
} else {
$package = new AliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized));
}
}
if ($this->loadOptions && isset($config['transport-options'])) {
$package->setTransportOptions($config['transport-options']);
}
if ($aliasNormalized = $this->getBranchAlias($config)) {
if ($package instanceof RootPackageInterface) {
return new RootAliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized));
}
return new AliasPackage($package, $aliasNormalized, preg_replace('{(\.9{7})+}', '.x', $aliasNormalized));
}
return $package;
}
private function configureCachedLinks(&$linkCache, $package, array $config)
{
$name = $package->getName();
$prettyVersion = $package->getPrettyVersion();
foreach (Package\BasePackage::$supportedLinkTypes as $type => $opts) {
if (isset($config[$type])) {
$method = 'set'.ucfirst($opts['method']);
$links = array();
foreach ($config[$type] as $prettyTarget => $constraint) {
$target = strtolower($prettyTarget);
if ($constraint === 'self.version') {
$links[$target] = $this->createLink($name, $prettyVersion, $opts['description'], $target, $constraint);
} else {
if (!isset($linkCache[$name][$type][$target][$constraint])) {
$linkCache[$name][$type][$target][$constraint] = array($target, $this->createLink($name, $prettyVersion, $opts['description'], $target, $constraint));
}
list($target, $link) = $linkCache[$name][$type][$target][$constraint];
$links[$target] = $link;
}
}
$package->{$method}($links);
}
}
}
/**
* @param string $source source package name
* @param string $sourceVersion source package version (pretty version ideally)
@ -229,21 +311,26 @@ class ArrayLoader implements LoaderInterface
{
$res = array();
foreach ($links as $target => $constraint) {
if (!is_string($constraint)) {
throw new \UnexpectedValueException('Link constraint in '.$source.' '.$description.' > '.$target.' should be a string, got '.gettype($constraint) . ' (' . var_export($constraint, true) . ')');
}
if ('self.version' === $constraint) {
$parsedConstraint = $this->versionParser->parseConstraints($sourceVersion);
} else {
$parsedConstraint = $this->versionParser->parseConstraints($constraint);
}
$res[strtolower($target)] = new Link($source, $target, $parsedConstraint, $description, $constraint);
$res[strtolower($target)] = $this->createLink($source, $sourceVersion, $description, $target, $constraint);
}
return $res;
}
private function createLink($source, $sourceVersion, $description, $target, $prettyConstraint)
{
if (!is_string($prettyConstraint)) {
throw new \UnexpectedValueException('Link constraint in '.$source.' '.$description.' > '.$target.' should be a string, got '.gettype($prettyConstraint) . ' (' . var_export($prettyConstraint, true) . ')');
}
if ('self.version' === $prettyConstraint) {
$parsedConstraint = $this->versionParser->parseConstraints($sourceVersion);
} else {
$parsedConstraint = $this->versionParser->parseConstraints($prettyConstraint);
}
return new Link($source, $target, $parsedConstraint, $description, $prettyConstraint);
}
/**
* Retrieves a branch alias (dev-master => 1.0.x-dev for example) if it exists
*

View File

@ -192,7 +192,8 @@ class VersionGuesser
}
// re-use the HgDriver to fetch branches (this properly includes bookmarks)
$driver = new HgDriver(array('url' => $path), new NullIO(), $this->config, $this->process);
$io = new NullIO();
$driver = new HgDriver(array('url' => $path), $io, $this->config, new HttpDownloader($io, $this->config), $this->process);
$branches = array_keys($driver->getBranches());
// try to find the best (nearest) version branch to assume this feature's version

View File

@ -13,7 +13,7 @@
namespace Composer\Plugin;
use Composer\EventDispatcher\Event;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
/**
* The pre file download event.
@ -23,9 +23,9 @@ use Composer\Util\RemoteFilesystem;
class PreFileDownloadEvent extends Event
{
/**
* @var RemoteFilesystem
* @var HttpDownloader
*/
private $rfs;
private $httpDownloader;
/**
* @var string
@ -36,34 +36,22 @@ class PreFileDownloadEvent extends Event
* Constructor.
*
* @param string $name The event name
* @param RemoteFilesystem $rfs
* @param HttpDownloader $httpDownloader
* @param string $processedUrl
*/
public function __construct($name, RemoteFilesystem $rfs, $processedUrl)
public function __construct($name, HttpDownloader $httpDownloader, $processedUrl)
{
parent::__construct($name);
$this->rfs = $rfs;
$this->httpDownloader = $httpDownloader;
$this->processedUrl = $processedUrl;
}
/**
* Returns the remote filesystem
*
* @return RemoteFilesystem
* @return HttpDownloader
*/
public function getRemoteFilesystem()
public function getHttpDownloader()
{
return $this->rfs;
}
/**
* Sets the remote filesystem
*
* @param RemoteFilesystem $rfs
*/
public function setRemoteFilesystem(RemoteFilesystem $rfs)
{
$this->rfs = $rfs;
return $this->httpDownloader;
}
/**

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@
namespace Composer\Repository\Pear;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
/**
* Base PEAR Channel reader.
@ -33,12 +33,12 @@ abstract class BaseChannelReader
const ALL_RELEASES_NS = 'http://pear.php.net/dtd/rest.allreleases';
const PACKAGE_INFO_NS = 'http://pear.php.net/dtd/rest.package';
/** @var RemoteFilesystem */
private $rfs;
/** @var HttpDownloader */
private $httpDownloader;
protected function __construct(RemoteFilesystem $rfs)
protected function __construct(HttpDownloader $httpDownloader)
{
$this->rfs = $rfs;
$this->httpDownloader = $httpDownloader;
}
/**
@ -52,7 +52,11 @@ abstract class BaseChannelReader
protected function requestContent($origin, $path)
{
$url = rtrim($origin, '/') . '/' . ltrim($path, '/');
$content = $this->rfs->getContents($origin, $url, false);
try {
$content = $this->httpDownloader->get($url)->getBody();
} catch (\Exception $e) {
throw new \UnexpectedValueException('The PEAR channel at ' . $url . ' did not respond.', 0, $e);
}
if (!$content) {
throw new \UnexpectedValueException('The PEAR channel at ' . $url . ' did not respond.');
}

View File

@ -12,7 +12,7 @@
namespace Composer\Repository\Pear;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
/**
* PEAR Channel package reader.
@ -26,12 +26,12 @@ class ChannelReader extends BaseChannelReader
/** @var array of ('xpath test' => 'rest implementation') */
private $readerMap;
public function __construct(RemoteFilesystem $rfs)
public function __construct(HttpDownloader $httpDownloader)
{
parent::__construct($rfs);
parent::__construct($httpDownloader);
$rest10reader = new ChannelRest10Reader($rfs);
$rest11reader = new ChannelRest11Reader($rfs);
$rest10reader = new ChannelRest10Reader($httpDownloader);
$rest11reader = new ChannelRest11Reader($httpDownloader);
$this->readerMap = array(
'REST1.3' => $rest11reader,

View File

@ -13,6 +13,7 @@
namespace Composer\Repository\Pear;
use Composer\Downloader\TransportException;
use Composer\Util\HttpDownloader;
/**
* Read PEAR packages using REST 1.0 interface
@ -29,9 +30,9 @@ class ChannelRest10Reader extends BaseChannelReader
{
private $dependencyReader;
public function __construct($rfs)
public function __construct(HttpDownloader $httpDownloader)
{
parent::__construct($rfs);
parent::__construct($httpDownloader);
$this->dependencyReader = new PackageDependencyParser();
}

View File

@ -12,6 +12,8 @@
namespace Composer\Repository\Pear;
use Composer\Util\HttpDownloader;
/**
* Read PEAR packages using REST 1.1 interface
*
@ -25,9 +27,9 @@ class ChannelRest11Reader extends BaseChannelReader
{
private $dependencyReader;
public function __construct($rfs)
public function __construct(HttpDownloader $httpDownloader)
{
parent::__construct($rfs);
parent::__construct($httpDownloader);
$this->dependencyReader = new PackageDependencyParser();
}

View File

@ -21,7 +21,7 @@ use Composer\Repository\Pear\ChannelInfo;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Package\Link;
use Composer\Semver\Constraint\Constraint;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\Config;
use Composer\Factory;
@ -38,7 +38,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn
{
private $url;
private $io;
private $rfs;
private $httpDownloader;
private $versionParser;
private $repoConfig;
@ -47,7 +47,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn
*/
private $vendorAlias;
public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $dispatcher = null, RemoteFilesystem $rfs = null)
public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $dispatcher = null)
{
parent::__construct();
if (!preg_match('{^https?://}', $repoConfig['url'])) {
@ -61,7 +61,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn
$this->url = rtrim($repoConfig['url'], '/');
$this->io = $io;
$this->rfs = $rfs ?: Factory::createRemoteFilesystem($this->io, $config);
$this->httpDownloader = $httpDownloader;
$this->vendorAlias = isset($repoConfig['vendor-alias']) ? $repoConfig['vendor-alias'] : null;
$this->versionParser = new VersionParser();
$this->repoConfig = $repoConfig;
@ -78,7 +78,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn
$this->io->writeError('Initializing PEAR repository '.$this->url);
$reader = new ChannelReader($this->rfs);
$reader = new ChannelReader($this->httpDownloader);
try {
$channelInfo = $reader->read($this->url);
} catch (\Exception $e) {

View File

@ -308,6 +308,10 @@ class PlatformRepository extends ArrayRepository
$this->addPackage($ext);
}
/**
* @param string $name
* @return string
*/
private function buildPackageName($name)
{
return 'ext-' . str_replace(' ', '-', $name);

View File

@ -16,7 +16,7 @@ use Composer\Factory;
use Composer\IO\IOInterface;
use Composer\Config;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\Json\JsonFile;
/**
@ -36,7 +36,7 @@ class RepositoryFactory
if (0 === strpos($repository, 'http')) {
$repoConfig = array('type' => 'composer', 'url' => $repository);
} elseif ("json" === pathinfo($repository, PATHINFO_EXTENSION)) {
$json = new JsonFile($repository, Factory::createRemoteFilesystem($io, $config));
$json = new JsonFile($repository, Factory::createHttpDownloader($io, $config));
$data = $json->read();
if (!empty($data['packages']) || !empty($data['includes']) || !empty($data['provider-includes'])) {
$repoConfig = array('type' => 'composer', 'url' => 'file://' . strtr(realpath($repository), '\\', '/'));
@ -77,7 +77,7 @@ class RepositoryFactory
*/
public static function createRepo(IOInterface $io, Config $config, array $repoConfig)
{
$rm = static::manager($io, $config, null, Factory::createRemoteFilesystem($io, $config));
$rm = static::manager($io, $config, Factory::createHttpDownloader($io, $config));
$repos = static::createRepos($rm, array($repoConfig));
return reset($repos);
@ -98,7 +98,7 @@ class RepositoryFactory
if (!$io) {
throw new \InvalidArgumentException('This function requires either an IOInterface or a RepositoryManager');
}
$rm = static::manager($io, $config, null, Factory::createRemoteFilesystem($io, $config));
$rm = static::manager($io, $config, Factory::createHttpDownloader($io, $config));
}
return static::createRepos($rm, $config->getRepositories());
@ -108,12 +108,12 @@ class RepositoryFactory
* @param IOInterface $io
* @param Config $config
* @param EventDispatcher $eventDispatcher
* @param RemoteFilesystem $rfs
* @param HttpDownloader $httpDownloader
* @return RepositoryManager
*/
public static function manager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null)
public static function manager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null)
{
$rm = new RepositoryManager($io, $config, $eventDispatcher, $rfs);
$rm = new RepositoryManager($io, $config, $httpDownloader, $eventDispatcher);
$rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository');
$rm->setRepositoryClass('vcs', 'Composer\Repository\VcsRepository');
$rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository');

View File

@ -13,6 +13,7 @@
namespace Composer\Repository;
use Composer\Package\PackageInterface;
use Composer\Semver\Constraint\ConstraintInterface;
/**
* Repository interface.
@ -38,8 +39,8 @@ interface RepositoryInterface extends \Countable
/**
* Searches for the first match of a package by name and version.
*
* @param string $name package name
* @param string|\Composer\Semver\Constraint\ConstraintInterface $constraint package version or version constraint to match against
* @param string $name package name
* @param string|ConstraintInterface $constraint package version or version constraint to match against
*
* @return PackageInterface|null
*/
@ -48,8 +49,8 @@ interface RepositoryInterface extends \Countable
/**
* Searches for all packages matching a name and optionally a version.
*
* @param string $name package name
* @param string|\Composer\Semver\Constraint\ConstraintInterface $constraint package version or version constraint to match against
* @param string $name package name
* @param string|ConstraintInterface $constraint package version or version constraint to match against
*
* @return PackageInterface[]
*/
@ -66,7 +67,7 @@ interface RepositoryInterface extends \Countable
/**
* Returns list of registered packages with the supplied name
*
* @param bool[] $packageNameMap
* @param ConstraintInterface[] $packageNameMap package names pointing to constraints
* @param $isPackageAcceptableCallable
* @return PackageInterface[]
*/

View File

@ -16,7 +16,7 @@ use Composer\IO\IOInterface;
use Composer\Config;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
/**
* Repositories manager.
@ -33,14 +33,14 @@ class RepositoryManager
private $io;
private $config;
private $eventDispatcher;
private $rfs;
private $httpDownloader;
public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null)
public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null)
{
$this->io = $io;
$this->config = $config;
$this->httpDownloader = $httpDownloader;
$this->eventDispatcher = $eventDispatcher;
$this->rfs = $rfs;
}
/**
@ -127,8 +127,8 @@ class RepositoryManager
$reflMethod = new \ReflectionMethod($class, '__construct');
$params = $reflMethod->getParameters();
if (isset($params[4]) && $params[4]->getClass() && $params[4]->getClass()->getName() === 'Composer\Util\RemoteFilesystem') {
return new $class($config, $this->io, $this->config, $this->eventDispatcher, $this->rfs);
if (isset($params[3]) && $params[3]->getClass() && $params[3]->getClass()->getName() === 'Composer\Util\HttpDownloader') {
return new $class($config, $this->io, $this->config, $this->httpDownloader, $this->eventDispatcher);
}
return new $class($config, $this->io, $this->config, $this->eventDispatcher);

View File

@ -16,6 +16,7 @@ use Composer\Cache;
use Composer\Downloader\TransportException;
use Composer\Json\JsonFile;
use Composer\Util\Bitbucket;
use Composer\Util\Http\Response;
abstract class BitbucketDriver extends VcsDriver
{
@ -92,7 +93,7 @@ abstract class BitbucketDriver extends VcsDriver
)
);
$repoData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource, true), $resource);
$repoData = $this->fetchWithOAuthCredentials($resource, true)->decodeJson();
if ($this->fallbackDriver) {
return false;
}
@ -204,7 +205,7 @@ abstract class BitbucketDriver extends VcsDriver
$file
);
return $this->getContentsWithOAuthCredentials($resource);
return $this->fetchWithOAuthCredentials($resource)->getBody();
}
/**
@ -222,7 +223,7 @@ abstract class BitbucketDriver extends VcsDriver
$this->repository,
$identifier
);
$commit = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource);
$commit = $this->fetchWithOAuthCredentials($resource)->decodeJson();
return new \DateTime($commit['date']);
}
@ -284,7 +285,7 @@ abstract class BitbucketDriver extends VcsDriver
);
$hasNext = true;
while ($hasNext) {
$tagsData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource);
$tagsData = $this->fetchWithOAuthCredentials($resource)->decodeJson();
foreach ($tagsData['values'] as $data) {
$this->tags[$data['name']] = $data['target']['hash'];
}
@ -328,7 +329,7 @@ abstract class BitbucketDriver extends VcsDriver
);
$hasNext = true;
while ($hasNext) {
$branchData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource);
$branchData = $this->fetchWithOAuthCredentials($resource)->decodeJson();
foreach ($branchData['values'] as $data) {
// skip headless branches which seem to be deleted branches that bitbucket nevertheless returns in the API
if ($this->vcsType === 'hg' && empty($data['heads'])) {
@ -354,14 +355,14 @@ abstract class BitbucketDriver extends VcsDriver
* @param string $url The URL of content
* @param bool $fetchingRepoData
*
* @return mixed The result
* @return Response The result
*/
protected function getContentsWithOAuthCredentials($url, $fetchingRepoData = false)
protected function fetchWithOAuthCredentials($url, $fetchingRepoData = false)
{
try {
return parent::getContents($url);
} catch (TransportException $e) {
$bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process, $this->remoteFilesystem);
$bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process, $this->httpDownloader);
if (403 === $e->getCode() || (401 === $e->getCode() && strpos($e->getMessage(), 'Could not authenticate against') === 0)) {
if (!$this->io->hasAuthentication($this->originUrl)
@ -371,7 +372,9 @@ abstract class BitbucketDriver extends VcsDriver
}
if (!$this->io->isInteractive() && $fetchingRepoData) {
return $this->attemptCloneFallback();
if ($this->attemptCloneFallback()) {
return new Response(array('url' => 'dummy'), 200, array(), 'null');
}
}
}
@ -390,6 +393,8 @@ abstract class BitbucketDriver extends VcsDriver
{
try {
$this->setupFallbackDriver($this->generateSshUrl());
return true;
} catch (\RuntimeException $e) {
$this->fallbackDriver = null;
@ -433,7 +438,7 @@ abstract class BitbucketDriver extends VcsDriver
$this->repository
);
$data = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource);
$data = $this->fetchWithOAuthCredentials($resource)->decodeJson();
if (isset($data['mainbranch'])) {
return $data['mainbranch'];
}

View File

@ -75,8 +75,8 @@ class GitBitbucketDriver extends BitbucketDriver
array('url' => $url),
$this->io,
$this->config,
$this->process,
$this->remoteFilesystem
$this->httpDownloader,
$this->process
);
$this->fallbackDriver->initialize();
}

View File

@ -18,6 +18,8 @@ use Composer\Json\JsonFile;
use Composer\Cache;
use Composer\IO\IOInterface;
use Composer\Util\GitHub;
use Composer\Util\Http\Response;
use Composer\Util\RemoteFilesystem;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
@ -184,7 +186,7 @@ class GitHubDriver extends VcsDriver
}
$resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/contents/' . $file . '?ref='.urlencode($identifier);
$resource = JsonFile::parseJson($this->getContents($resource));
$resource = $this->getContents($resource)->decodeJson();
if (empty($resource['content']) || $resource['encoding'] !== 'base64' || !($content = base64_decode($resource['content']))) {
throw new \RuntimeException('Could not retrieve ' . $file . ' for '.$identifier);
}
@ -202,7 +204,7 @@ class GitHubDriver extends VcsDriver
}
$resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/commits/'.urlencode($identifier);
$commit = JsonFile::parseJson($this->getContents($resource), $resource);
$commit = $this->getContents($resource)->decodeJson();
return new \DateTime($commit['commit']['committer']['date']);
}
@ -220,12 +222,13 @@ class GitHubDriver extends VcsDriver
$resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/tags?per_page=100';
do {
$tagsData = JsonFile::parseJson($this->getContents($resource), $resource);
$response = $this->getContents($resource);
$tagsData = $response->decodeJson();
foreach ($tagsData as $tag) {
$this->tags[$tag['name']] = $tag['commit']['sha'];
}
$resource = $this->getNextPage();
$resource = $this->getNextPage($response);
} while ($resource);
}
@ -247,7 +250,8 @@ class GitHubDriver extends VcsDriver
$branchBlacklist = array('gh-pages');
do {
$branchData = JsonFile::parseJson($this->getContents($resource), $resource);
$response = $this->getContents($resource);
$branchData = $response->decodeJson();
foreach ($branchData as $branch) {
$name = substr($branch['ref'], 11);
if (!in_array($name, $branchBlacklist)) {
@ -255,7 +259,7 @@ class GitHubDriver extends VcsDriver
}
}
$resource = $this->getNextPage();
$resource = $this->getNextPage($response);
} while ($resource);
}
@ -315,7 +319,7 @@ class GitHubDriver extends VcsDriver
try {
return parent::getContents($url);
} catch (TransportException $e) {
$gitHubUtil = new GitHub($this->io, $this->config, $this->process, $this->remoteFilesystem);
$gitHubUtil = new GitHub($this->io, $this->config, $this->process, $this->httpDownloader);
switch ($e->getCode()) {
case 401:
@ -330,16 +334,18 @@ class GitHubDriver extends VcsDriver
}
if (!$this->io->isInteractive()) {
return $this->attemptCloneFallback();
if ($this->attemptCloneFallback()) {
return new Response(array('url' => 'dummy'), 200, array(), 'null');
}
}
$scopesIssued = array();
$scopesNeeded = array();
if ($headers = $e->getHeaders()) {
if ($scopes = $this->remoteFilesystem->findHeaderValue($headers, 'X-OAuth-Scopes')) {
if ($scopes = RemoteFilesystem::findHeaderValue($headers, 'X-OAuth-Scopes')) {
$scopesIssued = explode(' ', $scopes);
}
if ($scopes = $this->remoteFilesystem->findHeaderValue($headers, 'X-Accepted-OAuth-Scopes')) {
if ($scopes = RemoteFilesystem::findHeaderValue($headers, 'X-Accepted-OAuth-Scopes')) {
$scopesNeeded = explode(' ', $scopes);
}
}
@ -358,7 +364,9 @@ class GitHubDriver extends VcsDriver
}
if (!$this->io->isInteractive() && $fetchingRepoData) {
return $this->attemptCloneFallback();
if ($this->attemptCloneFallback()) {
return new Response(array('url' => 'dummy'), 200, array(), 'null');
}
}
$rateLimited = $gitHubUtil->isRateLimited($e->getHeaders());
@ -404,7 +412,7 @@ class GitHubDriver extends VcsDriver
$repoDataUrl = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository;
$this->repoData = JsonFile::parseJson($this->getContents($repoDataUrl, true), $repoDataUrl);
$this->repoData = $this->getContents($repoDataUrl, true)->decodeJson();
if (null === $this->repoData && null !== $this->gitDriver) {
return;
}
@ -434,7 +442,7 @@ class GitHubDriver extends VcsDriver
// are not interactive) then we fallback to GitDriver.
$this->setupGitDriver($this->generateSshUrl());
return;
return true;
} catch (\RuntimeException $e) {
$this->gitDriver = null;
@ -449,23 +457,20 @@ class GitHubDriver extends VcsDriver
array('url' => $url),
$this->io,
$this->config,
$this->process,
$this->remoteFilesystem
$this->httpDownloader,
$this->process
);
$this->gitDriver->initialize();
}
protected function getNextPage()
protected function getNextPage(Response $response)
{
$headers = $this->remoteFilesystem->getLastHeaders();
foreach ($headers as $header) {
if (preg_match('{^link:\s*(.+?)\s*$}i', $header, $match)) {
$links = explode(',', $match[1]);
foreach ($links as $link) {
if (preg_match('{<(.+?)>; *rel="next"}', $link, $match)) {
return $match[1];
}
}
$header = $response->getHeader('link');
$links = explode(',', $header);
foreach ($links as $link) {
if (preg_match('{<(.+?)>; *rel="next"}', $link, $match)) {
return $match[1];
}
}
}

View File

@ -17,8 +17,9 @@ use Composer\Cache;
use Composer\IO\IOInterface;
use Composer\Json\JsonFile;
use Composer\Downloader\TransportException;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\Util\GitLab;
use Composer\Util\Http\Response;
/**
* Driver for GitLab API, use the Git driver for local checkouts.
@ -110,14 +111,14 @@ class GitLabDriver extends VcsDriver
}
/**
* Updates the RemoteFilesystem instance.
* Updates the HttpDownloader instance.
* Mainly useful for tests.
*
* @internal
*/
public function setRemoteFilesystem(RemoteFilesystem $remoteFilesystem)
public function setHttpDownloader(HttpDownloader $httpDownloader)
{
$this->remoteFilesystem = $remoteFilesystem;
$this->httpDownloader = $httpDownloader;
}
/**
@ -140,7 +141,7 @@ class GitLabDriver extends VcsDriver
$resource = $this->getApiUrl().'/repository/files/'.$this->urlEncodeAll($file).'/raw?ref='.$identifier;
try {
$content = $this->getContents($resource);
$content = $this->getContents($resource)->getBody();
} catch (TransportException $e) {
if ($e->getCode() !== 404) {
throw $e;
@ -297,7 +298,8 @@ class GitLabDriver extends VcsDriver
$references = array();
do {
$data = JsonFile::parseJson($this->getContents($resource), $resource);
$response = $this->getContents($resource);
$data = $response->decodeJson();
foreach ($data as $datum) {
$references[$datum['name']] = $datum['commit']['id'];
@ -308,7 +310,7 @@ class GitLabDriver extends VcsDriver
}
if (count($data) >= $perPage) {
$resource = $this->getNextPage();
$resource = $this->getNextPage($response);
} else {
$resource = false;
}
@ -321,7 +323,7 @@ class GitLabDriver extends VcsDriver
{
// we need to fetch the default branch from the api
$resource = $this->getApiUrl();
$this->project = JsonFile::parseJson($this->getContents($resource, true), $resource);
$this->project = $this->getContents($resource, true)->decodeJson();
if (isset($this->project['visibility'])) {
$this->isPrivate = $this->project['visibility'] !== 'public';
} else {
@ -344,7 +346,7 @@ class GitLabDriver extends VcsDriver
// are not interactive) then we fallback to GitDriver.
$this->setupGitDriver($url);
return;
return true;
} catch (\RuntimeException $e) {
$this->gitDriver = null;
@ -374,8 +376,8 @@ class GitLabDriver extends VcsDriver
array('url' => $url),
$this->io,
$this->config,
$this->process,
$this->remoteFilesystem
$this->httpDownloader,
$this->process
);
$this->gitDriver->initialize();
}
@ -386,10 +388,10 @@ class GitLabDriver extends VcsDriver
protected function getContents($url, $fetchingRepoData = false)
{
try {
$res = parent::getContents($url);
$response = parent::getContents($url);
if ($fetchingRepoData) {
$json = JsonFile::parseJson($res, $url);
$json = $response->decodeJson();
// force auth as the unauthenticated version of the API is broken
if (!isset($json['default_branch'])) {
@ -401,9 +403,9 @@ class GitLabDriver extends VcsDriver
}
}
return $res;
return $response;
} catch (TransportException $e) {
$gitLabUtil = new GitLab($this->io, $this->config, $this->process, $this->remoteFilesystem);
$gitLabUtil = new GitLab($this->io, $this->config, $this->process, $this->httpDownloader);
switch ($e->getCode()) {
case 401:
@ -418,7 +420,9 @@ class GitLabDriver extends VcsDriver
}
if (!$this->io->isInteractive()) {
return $this->attemptCloneFallback();
if ($this->attemptCloneFallback()) {
return new Response(array('url' => 'dummy'), 200, array(), 'null');
}
}
$this->io->writeError('<warning>Failed to download ' . $this->namespace . '/' . $this->repository . ':' . $e->getMessage() . '</warning>');
$gitLabUtil->authorizeOAuthInteractively($this->scheme, $this->originUrl, 'Your credentials are required to fetch private repository metadata (<info>'.$this->url.'</info>)');
@ -431,7 +435,9 @@ class GitLabDriver extends VcsDriver
}
if (!$this->io->isInteractive() && $fetchingRepoData) {
return $this->attemptCloneFallback();
if ($this->attemptCloneFallback()) {
return new Response(array('url' => 'dummy'), 200, array(), 'null');
}
}
throw $e;
@ -471,17 +477,14 @@ class GitLabDriver extends VcsDriver
return true;
}
private function getNextPage()
protected function getNextPage(Response $response)
{
$headers = $this->remoteFilesystem->getLastHeaders();
foreach ($headers as $header) {
if (preg_match('{^link:\s*(.+?)\s*$}i', $header, $match)) {
$links = explode(',', $match[1]);
foreach ($links as $link) {
if (preg_match('{<(.+?)>; *rel="next"}', $link, $match)) {
return $match[1];
}
}
$header = $response->getHeader('link');
$links = explode(',', $header);
foreach ($links as $link) {
if (preg_match('{<(.+?)>; *rel="next"}', $link, $match)) {
return $match[1];
}
}
}

View File

@ -75,8 +75,8 @@ class HgBitbucketDriver extends BitbucketDriver
array('url' => $url),
$this->io,
$this->config,
$this->process,
$this->remoteFilesystem
$this->httpDownloader,
$this->process
);
$this->fallbackDriver->initialize();
}

View File

@ -19,8 +19,9 @@ use Composer\Factory;
use Composer\IO\IOInterface;
use Composer\Json\JsonFile;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\Util\Filesystem;
use Composer\Util\Http\Response;
/**
* A driver implementation for driver with authentication interaction.
@ -41,8 +42,8 @@ abstract class VcsDriver implements VcsDriverInterface
protected $config;
/** @var ProcessExecutor */
protected $process;
/** @var RemoteFilesystem */
protected $remoteFilesystem;
/** @var HttpDownloader */
protected $httpDownloader;
/** @var array */
protected $infoCache = array();
/** @var Cache */
@ -54,10 +55,10 @@ abstract class VcsDriver implements VcsDriverInterface
* @param array $repoConfig The repository configuration
* @param IOInterface $io The IO instance
* @param Config $config The composer configuration
* @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking
* @param ProcessExecutor $process Process instance, injectable for mocking
* @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking
*/
final public function __construct(array $repoConfig, IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null)
final public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, ProcessExecutor $process)
{
if (Filesystem::isLocalPath($repoConfig['url'])) {
$repoConfig['url'] = Filesystem::getPlatformPath($repoConfig['url']);
@ -68,8 +69,8 @@ abstract class VcsDriver implements VcsDriverInterface
$this->repoConfig = $repoConfig;
$this->io = $io;
$this->config = $config;
$this->process = $process ?: new ProcessExecutor($io);
$this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config);
$this->httpDownloader = $httpDownloader;
$this->process = $process;
}
/**
@ -156,13 +157,13 @@ abstract class VcsDriver implements VcsDriverInterface
*
* @param string $url The URL of content
*
* @return mixed The result
* @return Response
*/
protected function getContents($url)
{
$options = isset($this->repoConfig['options']) ? $this->repoConfig['options'] : array();
return $this->remoteFilesystem->getContents($this->originUrl, $url, false, $options);
return $this->httpDownloader->get($url, $options);
}
/**

View File

@ -20,6 +20,8 @@ use Composer\Package\Loader\ValidatingArrayLoader;
use Composer\Package\Loader\InvalidPackageException;
use Composer\Package\Loader\LoaderInterface;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\ProcessExecutor;
use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface;
use Composer\Config;
@ -37,6 +39,8 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
protected $type;
protected $loader;
protected $repoConfig;
protected $httpDownloader;
protected $processExecutor;
protected $branchErrorOccurred = false;
private $drivers;
/** @var VcsDriverInterface */
@ -44,7 +48,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
/** @var VersionCacheInterface */
private $versionCache;
public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $dispatcher = null, array $drivers = null, VersionCacheInterface $versionCache = null)
public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $dispatcher = null, array $drivers = null, VersionCacheInterface $versionCache = null)
{
parent::__construct();
$this->drivers = $drivers ?: array(
@ -67,6 +71,8 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
$this->config = $config;
$this->repoConfig = $repoConfig;
$this->versionCache = $versionCache;
$this->httpDownloader = $httpDownloader;
$this->processExecutor = new ProcessExecutor($io);
}
public function getRepoConfig()
@ -87,7 +93,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
if (isset($this->drivers[$this->type])) {
$class = $this->drivers[$this->type];
$this->driver = new $class($this->repoConfig, $this->io, $this->config);
$this->driver = new $class($this->repoConfig, $this->io, $this->config, $this->httpDownloader, $this->processExecutor);
$this->driver->initialize();
return $this->driver;
@ -95,7 +101,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
foreach ($this->drivers as $driver) {
if ($driver::supports($this->io, $this->config, $this->url)) {
$this->driver = new $driver($this->repoConfig, $this->io, $this->config);
$this->driver = new $driver($this->repoConfig, $this->io, $this->config, $this->httpDownloader, $this->processExecutor);
$this->driver->initialize();
return $this->driver;
@ -104,7 +110,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
foreach ($this->drivers as $driver) {
if ($driver::supports($this->io, $this->config, $this->url, true)) {
$this->driver = new $driver($this->repoConfig, $this->io, $this->config);
$this->driver = new $driver($this->repoConfig, $this->io, $this->config, $this->httpDownloader, $this->processExecutor);
$this->driver->initialize();
return $this->driver;

View File

@ -12,7 +12,7 @@
namespace Composer\SelfUpdate;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\Config;
use Composer\Json\JsonFile;
@ -21,13 +21,13 @@ use Composer\Json\JsonFile;
*/
class Versions
{
private $rfs;
private $httpDownloader;
private $config;
private $channel;
public function __construct(Config $config, RemoteFilesystem $rfs)
public function __construct(Config $config, HttpDownloader $httpDownloader)
{
$this->rfs = $rfs;
$this->httpDownloader = $httpDownloader;
$this->config = $config;
}
@ -62,7 +62,7 @@ class Versions
public function getLatest()
{
$protocol = extension_loaded('openssl') ? 'https' : 'http';
$versions = JsonFile::parseJson($this->rfs->getContents('getcomposer.org', $protocol . '://getcomposer.org/versions', false));
$versions = $this->httpDownloader->get($protocol . '://getcomposer.org/versions')->decodeJson();
foreach ($versions[$this->getChannel()] as $version) {
if ($version['min-php'] <= PHP_VERSION_ID) {

View File

@ -14,6 +14,7 @@ namespace Composer\Util;
use Composer\Config;
use Composer\IO\IOInterface;
use Composer\Downloader\TransportException;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
@ -29,7 +30,11 @@ class AuthHelper
$this->config = $config;
}
public function storeAuth($originUrl, $storeAuth)
/**
* @param string $origin
* @param string|bool $storeAuth
*/
public function storeAuth($origin, $storeAuth)
{
$store = false;
$configSource = $this->config->getAuthConfigSource();
@ -37,7 +42,7 @@ class AuthHelper
$store = $configSource;
} elseif ($storeAuth === 'prompt') {
$answer = $this->io->askAndValidate(
'Do you want to store credentials for '.$originUrl.' in '.$configSource->getName().' ? [Yn] ',
'Do you want to store credentials for '.$origin.' in '.$configSource->getName().' ? [Yn] ',
function ($value) {
$input = strtolower(substr(trim($value), 0, 1));
if (in_array($input, array('y','n'))) {
@ -55,9 +60,192 @@ class AuthHelper
}
if ($store) {
$store->addConfigSetting(
'http-basic.'.$originUrl,
$this->io->getAuthentication($originUrl)
'http-basic.'.$origin,
$this->io->getAuthentication($origin)
);
}
}
/**
* @param string $url
* @param string $origin
* @param int $statusCode HTTP status code that triggered this call
* @param string|null $reason a message/description explaining why this was called
* @param string $warning an authentication warning returned by the server as {"warning": ".."}, if present
* @param string[] $headers
* @return array containing retry (bool) and storeAuth (string|bool) keys, if retry is true the request should be
* retried, if storeAuth is true then on a successful retry the authentication should be persisted to auth.json
*/
public function promptAuthIfNeeded($url, $origin, $statusCode, $reason = null, $warning = null, $headers = array())
{
$storeAuth = false;
$retry = false;
if (in_array($origin, $this->config->get('github-domains'), true)) {
$gitHubUtil = new GitHub($this->io, $this->config, null);
$message = "\n";
$rateLimited = $gitHubUtil->isRateLimited($headers);
if ($rateLimited) {
$rateLimit = $gitHubUtil->getRateLimit($headers);
if ($this->io->hasAuthentication($origin)) {
$message = 'Review your configured GitHub OAuth token or enter a new one to go over the API rate limit.';
} else {
$message = 'Create a GitHub OAuth token to go over the API rate limit.';
}
$message = sprintf(
'GitHub API limit (%d calls/hr) is exhausted, could not fetch '.$url.'. '.$message.' You can also wait until %s for the rate limit to reset.',
$rateLimit['limit'],
$rateLimit['reset']
)."\n";
} else {
$message .= 'Could not fetch '.$url.', please ';
if ($this->io->hasAuthentication($origin)) {
$message .= 'review your configured GitHub OAuth token or enter a new one to access private repos';
} else {
$message .= 'create a GitHub OAuth token to access private repos';
}
}
if (!$gitHubUtil->authorizeOAuth($origin)
&& (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($origin, $message))
) {
throw new TransportException('Could not authenticate against '.$origin, 401);
}
} elseif (in_array($origin, $this->config->get('gitlab-domains'), true)) {
$message = "\n".'Could not fetch '.$url.', enter your ' . $origin . ' credentials ' .($statusCode === 401 ? 'to access private repos' : 'to go over the API rate limit');
$gitLabUtil = new GitLab($this->io, $this->config, null);
if ($this->io->hasAuthentication($origin) && ($auth = $this->io->getAuthentication($origin)) && $auth['password'] === 'private-token') {
throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $statusCode);
}
if (!$gitLabUtil->authorizeOAuth($origin)
&& (!$this->io->isInteractive() || !$gitLabUtil->authorizeOAuthInteractively(parse_url($url, PHP_URL_SCHEME), $origin, $message))
) {
throw new TransportException('Could not authenticate against '.$origin, 401);
}
} elseif ($origin === 'bitbucket.org') {
$askForOAuthToken = true;
if ($this->io->hasAuthentication($origin)) {
$auth = $this->io->getAuthentication($origin);
if ($auth['username'] !== 'x-token-auth') {
$bitbucketUtil = new Bitbucket($this->io, $this->config);
$accessToken = $bitbucketUtil->requestToken($origin, $auth['username'], $auth['password']);
if (!empty($accessToken)) {
$this->io->setAuthentication($origin, 'x-token-auth', $accessToken);
$askForOAuthToken = false;
}
} else {
throw new TransportException('Could not authenticate against ' . $origin, 401);
}
}
if ($askForOAuthToken) {
$message = "\n".'Could not fetch ' . $url . ', please create a bitbucket OAuth token to ' . (($statusCode === 401 || $statusCode === 403) ? 'access private repos' : 'go over the API rate limit');
$bitBucketUtil = new Bitbucket($this->io, $this->config);
if (! $bitBucketUtil->authorizeOAuth($origin)
&& (! $this->io->isInteractive() || !$bitBucketUtil->authorizeOAuthInteractively($origin, $message))
) {
throw new TransportException('Could not authenticate against ' . $origin, 401);
}
}
} else {
// 404s are only handled for github
if ($statusCode === 404) {
return;
}
// fail if the console is not interactive
if (!$this->io->isInteractive()) {
if ($statusCode === 401) {
$message = "The '" . $url . "' URL required authentication.\nYou must be using the interactive console to authenticate";
}
if ($statusCode === 403) {
$message = "The '" . $url . "' URL could not be accessed: " . $reason;
}
throw new TransportException($message, $statusCode);
}
// fail if we already have auth
if ($this->io->hasAuthentication($origin)) {
throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $statusCode);
}
$this->io->overwriteError('');
if ($warning) {
$this->io->writeError(' <warning>'.$warning.'</warning>');
}
$this->io->writeError(' Authentication required (<info>'.parse_url($url, PHP_URL_HOST).'</info>):');
$username = $this->io->ask(' Username: ');
$password = $this->io->askAndHideAnswer(' Password: ');
$this->io->setAuthentication($origin, $username, $password);
$storeAuth = $this->config->get('store-auths');
}
$retry = true;
return array('retry' => $retry, 'storeAuth' => $storeAuth);
}
/**
* @param array $headers
* @param string $origin
* @param string $url
* @return array updated headers array
*/
public function addAuthenticationHeader(array $headers, $origin, $url)
{
if ($this->io->hasAuthentication($origin)) {
$auth = $this->io->getAuthentication($origin);
if ('github.com' === $origin && 'x-oauth-basic' === $auth['password']) {
$headers[] = 'Authorization: token '.$auth['username'];
} elseif (in_array($origin, $this->config->get('gitlab-domains'), true)) {
if ($auth['password'] === 'oauth2') {
$headers[] = 'Authorization: Bearer '.$auth['username'];
} elseif ($auth['password'] === 'private-token') {
$headers[] = 'PRIVATE-TOKEN: '.$auth['username'];
}
} elseif (
'bitbucket.org' === $origin
&& $url !== Bitbucket::OAUTH2_ACCESS_TOKEN_URL
&& 'x-token-auth' === $auth['username']
) {
if (!$this->isPublicBitBucketDownload($url)) {
$headers[] = 'Authorization: Bearer ' . $auth['password'];
}
} else {
$authStr = base64_encode($auth['username'] . ':' . $auth['password']);
$headers[] = 'Authorization: Basic '.$authStr;
}
}
return $headers;
}
/**
* @link https://github.com/composer/composer/issues/5584
*
* @param string $urlToBitBucketFile URL to a file at bitbucket.org.
*
* @return bool Whether the given URL is a public BitBucket download which requires no authentication.
*/
public function isPublicBitBucketDownload($urlToBitBucketFile)
{
$domain = parse_url($urlToBitBucketFile, PHP_URL_HOST);
if (strpos($domain, 'bitbucket.org') === false) {
// Bitbucket downloads are hosted on amazonaws.
// We do not need to authenticate there at all
return true;
}
$path = parse_url($urlToBitBucketFile, PHP_URL_PATH);
// Path for a public download follows this pattern /{user}/{repo}/downloads/{whatever}
// {@link https://blog.bitbucket.org/2009/04/12/new-feature-downloads/}
$pathParts = explode('/', $path);
return count($pathParts) >= 4 && $pathParts[3] == 'downloads';
}
}

View File

@ -25,7 +25,7 @@ class Bitbucket
private $io;
private $config;
private $process;
private $remoteFilesystem;
private $httpDownloader;
private $token = array();
private $time;
@ -37,15 +37,15 @@ class Bitbucket
* @param IOInterface $io The IO instance
* @param Config $config The composer configuration
* @param ProcessExecutor $process Process instance, injectable for mocking
* @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking
* @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking
* @param int $time Timestamp, injectable for mocking
*/
public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null, $time = null)
public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, HttpDownloader $httpDownloader = null, $time = null)
{
$this->io = $io;
$this->config = $config;
$this->process = $process ?: new ProcessExecutor($io);
$this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config);
$this->httpDownloader = $httpDownloader ?: Factory::createHttpDownloader($this->io, $config);
$this->time = $time;
}
@ -90,7 +90,7 @@ class Bitbucket
private function requestAccessToken($originUrl)
{
try {
$json = $this->remoteFilesystem->getContents($originUrl, self::OAUTH2_ACCESS_TOKEN_URL, false, array(
$response = $this->httpDownloader->get(self::OAUTH2_ACCESS_TOKEN_URL, array(
'retry-auth-failure' => false,
'http' => array(
'method' => 'POST',
@ -98,7 +98,7 @@ class Bitbucket
),
));
$this->token = json_decode($json, true);
$this->token = $response->decodeJson();
} catch (TransportException $e) {
if ($e->getCode() === 400) {
$this->io->writeError('<error>Invalid OAuth consumer provided.</error>');

View File

@ -25,7 +25,7 @@ class GitHub
protected $io;
protected $config;
protected $process;
protected $remoteFilesystem;
protected $httpDownloader;
/**
* Constructor.
@ -33,14 +33,14 @@ class GitHub
* @param IOInterface $io The IO instance
* @param Config $config The composer configuration
* @param ProcessExecutor $process Process instance, injectable for mocking
* @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking
* @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking
*/
public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null)
public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, HttpDownloader $httpDownloader = null)
{
$this->io = $io;
$this->config = $config;
$this->process = $process ?: new ProcessExecutor($io);
$this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config);
$this->httpDownloader = $httpDownloader ?: Factory::createHttpDownloader($this->io, $config);
}
/**
@ -104,7 +104,7 @@ class GitHub
try {
$apiUrl = ('github.com' === $originUrl) ? 'api.github.com/' : $originUrl . '/api/v3/';
$this->remoteFilesystem->getContents($originUrl, 'https://'. $apiUrl, false, array(
$this->httpDownloader->get('https://'. $apiUrl, array(
'retry-auth-failure' => false,
));
} catch (TransportException $e) {

View File

@ -26,7 +26,7 @@ class GitLab
protected $io;
protected $config;
protected $process;
protected $remoteFilesystem;
protected $httpDownloader;
/**
* Constructor.
@ -34,14 +34,14 @@ class GitLab
* @param IOInterface $io The IO instance
* @param Config $config The composer configuration
* @param ProcessExecutor $process Process instance, injectable for mocking
* @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking
* @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking
*/
public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null)
public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, HttpDownloader $httpDownloader = null)
{
$this->io = $io;
$this->config = $config;
$this->process = $process ?: new ProcessExecutor($io);
$this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config);
$this->httpDownloader = $httpDownloader ?: Factory::createHttpDownloader($this->io, $config);
}
/**
@ -154,10 +154,10 @@ class GitLab
),
);
$json = $this->remoteFilesystem->getContents($originUrl, $scheme.'://'.$apiUrl.'/oauth/token', false, $options);
$token = $this->httpDownloader->get($scheme.'://'.$apiUrl.'/oauth/token', $options)->decodeJson();
$this->io->writeError('Token successfully created');
return JsonFile::parseJson($json);
return $token;
}
}

View File

@ -0,0 +1,463 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Util\Http;
use Composer\Config;
use Composer\IO\IOInterface;
use Composer\Downloader\TransportException;
use Composer\CaBundle\CaBundle;
use Composer\Util\RemoteFilesystem;
use Composer\Util\StreamContextFactory;
use Composer\Util\AuthHelper;
use Composer\Util\Url;
use Psr\Log\LoggerInterface;
use React\Promise\Promise;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
* @author Nicolas Grekas <p@tchwork.com>
*/
class CurlDownloader
{
private $multiHandle;
private $shareHandle;
private $jobs = array();
/** @var IOInterface */
private $io;
/** @var Config */
private $config;
/** @var AuthHelper */
private $authHelper;
private $selectTimeout = 5.0;
private $maxRedirects = 20;
protected $multiErrors = array(
CURLM_BAD_HANDLE => array('CURLM_BAD_HANDLE', 'The passed-in handle is not a valid CURLM handle.'),
CURLM_BAD_EASY_HANDLE => array('CURLM_BAD_EASY_HANDLE', "An easy handle was not good/valid. It could mean that it isn't an easy handle at all, or possibly that the handle already is in used by this or another multi handle."),
CURLM_OUT_OF_MEMORY => array('CURLM_OUT_OF_MEMORY', 'You are doomed.'),
CURLM_INTERNAL_ERROR => array('CURLM_INTERNAL_ERROR', 'This can only be returned if libcurl bugs. Please report it to us!')
);
private static $options = array(
'http' => array(
'method' => CURLOPT_CUSTOMREQUEST,
'content' => CURLOPT_POSTFIELDS,
'proxy' => CURLOPT_PROXY,
'header' => CURLOPT_HTTPHEADER,
),
'ssl' => array(
'ciphers' => CURLOPT_SSL_CIPHER_LIST,
'cafile' => CURLOPT_CAINFO,
'capath' => CURLOPT_CAPATH,
),
);
private static $timeInfo = array(
'total_time' => true,
'namelookup_time' => true,
'connect_time' => true,
'pretransfer_time' => true,
'starttransfer_time' => true,
'redirect_time' => true,
);
public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false)
{
$this->io = $io;
$this->config = $config;
$this->multiHandle = $mh = curl_multi_init();
if (function_exists('curl_multi_setopt')) {
curl_multi_setopt($mh, CURLMOPT_PIPELINING, /*CURLPIPE_HTTP1 | CURLPIPE_MULTIPLEX*/ 3);
if (defined('CURLMOPT_MAX_HOST_CONNECTIONS')) {
curl_multi_setopt($mh, CURLMOPT_MAX_HOST_CONNECTIONS, 8);
}
}
if (function_exists('curl_share_init')) {
$this->shareHandle = $sh = curl_share_init();
curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
}
$this->authHelper = new AuthHelper($io, $config);
}
public function download($resolve, $reject, $origin, $url, $options, $copyTo = null)
{
$attributes = array();
if (isset($options['retry-auth-failure'])) {
$attributes['retryAuthFailure'] = $options['retry-auth-failure'];
unset($options['retry-auth-failure']);
}
return $this->initDownload($resolve, $reject, $origin, $url, $options, $copyTo, $attributes);
}
private function initDownload($resolve, $reject, $origin, $url, $options, $copyTo = null, array $attributes = array())
{
$attributes = array_merge(array(
'retryAuthFailure' => true,
'redirects' => 0,
'storeAuth' => false,
), $attributes);
$originalOptions = $options;
// check URL can be accessed (i.e. is not insecure), but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256
if (!preg_match('{^http://(repo\.)?packagist\.org/p/}', $url) || (false === strpos($url, '$') && false === strpos($url, '%24'))) {
$this->config->prohibitUrlByConfig($url, $this->io);
}
$curlHandle = curl_init();
$headerHandle = fopen('php://temp/maxmemory:32768', 'w+b');
if ($copyTo) {
$errorMessage = '';
set_error_handler(function ($code, $msg) use (&$errorMessage) {
if ($errorMessage) {
$errorMessage .= "\n";
}
$errorMessage .= preg_replace('{^fopen\(.*?\): }', '', $msg);
});
$bodyHandle = fopen($copyTo.'~', 'w+b');
restore_error_handler();
if (!$bodyHandle) {
throw new TransportException('The "'.$url.'" file could not be written to '.$copyTo.': '.$errorMessage);
}
} else {
$bodyHandle = @fopen('php://temp/maxmemory:524288', 'w+b');
}
curl_setopt($curlHandle, CURLOPT_URL, $url);
curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, false);
//curl_setopt($curlHandle, CURLOPT_DNS_USE_GLOBAL_CACHE, false);
curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($curlHandle, CURLOPT_TIMEOUT, 60);
curl_setopt($curlHandle, CURLOPT_WRITEHEADER, $headerHandle);
curl_setopt($curlHandle, CURLOPT_FILE, $bodyHandle);
curl_setopt($curlHandle, CURLOPT_ENCODING, "gzip");
curl_setopt($curlHandle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP|CURLPROTO_HTTPS);
if (defined('CURLOPT_SSL_FALSESTART')) {
curl_setopt($curlHandle, CURLOPT_SSL_FALSESTART, true);
}
if (function_exists('curl_share_init')) {
curl_setopt($curlHandle, CURLOPT_SHARE, $this->shareHandle);
}
if (!isset($options['http']['header'])) {
$options['http']['header'] = array();
}
$options['http']['header'] = array_diff($options['http']['header'], array('Connection: close'));
$options['http']['header'][] = 'Connection: keep-alive';
$version = curl_version();
$features = $version['features'];
if (0 === strpos($url, 'https://') && \defined('CURL_VERSION_HTTP2') && \defined('CURL_HTTP_VERSION_2_0') && (CURL_VERSION_HTTP2 & $features)) {
curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
}
$options['http']['header'] = $this->authHelper->addAuthenticationHeader($options['http']['header'], $origin, $url);
$options = StreamContextFactory::initOptions($url, $options);
foreach (self::$options as $type => $curlOptions) {
foreach ($curlOptions as $name => $curlOption) {
if (isset($options[$type][$name])) {
curl_setopt($curlHandle, $curlOption, $options[$type][$name]);
}
}
}
$progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo);
$this->jobs[(int) $curlHandle] = array(
'url' => $url,
'origin' => $origin,
'attributes' => $attributes,
'options' => $originalOptions,
'progress' => $progress,
'curlHandle' => $curlHandle,
'filename' => $copyTo,
'headerHandle' => $headerHandle,
'bodyHandle' => $bodyHandle,
'resolve' => $resolve,
'reject' => $reject,
);
$usingProxy = !empty($options['http']['proxy']) ? ' using proxy ' . $options['http']['proxy'] : '';
$ifModified = false !== strpos(strtolower(implode(',', $options['http']['header'])), 'if-modified-since:') ? ' if modified' : '';
if ($attributes['redirects'] === 0) {
$this->io->writeError('Downloading ' . $url . $usingProxy . $ifModified, true, IOInterface::DEBUG);
}
$this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $curlHandle));
// TODO progress
//$params['notification'](STREAM_NOTIFY_RESOLVE, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0, false);
}
public function tick()
{
if (!$this->jobs) {
return;
}
$active = true;
$this->checkCurlResult(curl_multi_exec($this->multiHandle, $active));
if (-1 === curl_multi_select($this->multiHandle, $this->selectTimeout)) {
// sleep in case select returns -1 as it can happen on old php versions or some platforms where curl does not manage to do the select
usleep(150);
}
while ($progress = curl_multi_info_read($this->multiHandle)) {
$curlHandle = $progress['handle'];
$i = (int) $curlHandle;
if (!isset($this->jobs[$i])) {
continue;
}
$progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo);
$job = $this->jobs[$i];
unset($this->jobs[$i]);
curl_multi_remove_handle($this->multiHandle, $curlHandle);
$error = curl_error($curlHandle);
$errno = curl_errno($curlHandle);
curl_close($curlHandle);
$headers = null;
$statusCode = null;
$response = null;
try {
// TODO progress
//$this->onProgress($curlHandle, $job['callback'], $progress, $job['progress']);
if (CURLE_OK !== $errno || $error) {
throw new TransportException($error);
}
$statusCode = $progress['http_code'];
rewind($job['headerHandle']);
$headers = explode("\r\n", rtrim(stream_get_contents($job['headerHandle'])));
fclose($job['headerHandle']);
// prepare response object
if ($job['filename']) {
fclose($job['bodyHandle']);
$response = new Response(array('url' => $progress['url']), $statusCode, $headers, $job['filename'].'~');
$this->io->writeError('['.$statusCode.'] '.$progress['url'], true, IOInterface::DEBUG);
} else {
rewind($job['bodyHandle']);
$contents = stream_get_contents($job['bodyHandle']);
fclose($job['bodyHandle']);
$response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents);
$this->io->writeError('['.$statusCode.'] '.$progress['url'], true, IOInterface::DEBUG);
}
$result = $this->isAuthenticatedRetryNeeded($job, $response);
if ($result['retry']) {
if ($job['filename']) {
@unlink($job['filename'].'~');
}
$this->restartJob($job, $job['url'], array('storeAuth' => $result['storeAuth']));
continue;
}
// handle 3xx redirects, 304 Not Modified is excluded
if ($statusCode >= 300 && $statusCode <= 399 && $statusCode !== 304 && $job['attributes']['redirects'] < $this->maxRedirects) {
$location = $this->handleRedirect($job, $response);
if ($location) {
$this->restartJob($job, $location, array('redirects' => $job['attributes']['redirects'] + 1));
continue;
}
}
// fail 4xx and 5xx responses and capture the response
if ($statusCode >= 400 && $statusCode <= 599) {
throw $this->failResponse($job, $response, $response->getStatusMessage());
// TODO progress
// $this->io->overwriteError("Downloading (<error>failed</error>)", false);
}
if ($job['attributes']['storeAuth']) {
$this->authHelper->storeAuth($job['origin'], $job['attributes']['storeAuth']);
}
// resolve promise
if ($job['filename']) {
rename($job['filename'].'~', $job['filename']);
call_user_func($job['resolve'], $response);
} else {
call_user_func($job['resolve'], $response);
}
} catch (\Exception $e) {
if ($e instanceof TransportException && $headers) {
$e->setHeaders($headers);
$e->setStatusCode($statusCode);
}
if ($e instanceof TransportException && $response) {
$e->setResponse($response->getBody());
}
if (is_resource($job['headerHandle'])) {
fclose($job['headerHandle']);
}
if (is_resource($job['bodyHandle'])) {
fclose($job['bodyHandle']);
}
if ($job['filename']) {
@unlink($job['filename'].'~');
}
call_user_func($job['reject'], $e);
}
}
foreach ($this->jobs as $i => $curlHandle) {
if (!isset($this->jobs[$i])) {
continue;
}
$curlHandle = $this->jobs[$i]['curlHandle'];
$progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo);
if ($this->jobs[$i]['progress'] !== $progress) {
$previousProgress = $this->jobs[$i]['progress'];
$this->jobs[$i]['progress'] = $progress;
// TODO
//$this->onProgress($curlHandle, $this->jobs[$i]['callback'], $progress, $previousProgress);
}
}
}
private function handleRedirect(array $job, Response $response)
{
if ($locationHeader = $response->getHeader('location')) {
if (parse_url($locationHeader, PHP_URL_SCHEME)) {
// Absolute URL; e.g. https://example.com/composer
$targetUrl = $locationHeader;
} elseif (parse_url($locationHeader, PHP_URL_HOST)) {
// Scheme relative; e.g. //example.com/foo
$targetUrl = parse_url($job['url'], PHP_URL_SCHEME).':'.$locationHeader;
} elseif ('/' === $locationHeader[0]) {
// Absolute path; e.g. /foo
$urlHost = parse_url($job['url'], PHP_URL_HOST);
// Replace path using hostname as an anchor.
$targetUrl = preg_replace('{^(.+(?://|@)'.preg_quote($urlHost).'(?::\d+)?)(?:[/\?].*)?$}', '\1'.$locationHeader, $job['url']);
} else {
// Relative path; e.g. foo
// This actually differs from PHP which seems to add duplicate slashes.
$targetUrl = preg_replace('{^(.+/)[^/?]*(?:\?.*)?$}', '\1'.$locationHeader, $job['url']);
}
}
if (!empty($targetUrl)) {
$this->io->writeError(sprintf('Following redirect (%u) %s', $job['attributes']['redirects'] + 1, $targetUrl), true, IOInterface::DEBUG);
return $targetUrl;
}
throw new TransportException('The "'.$job['url'].'" file could not be downloaded, got redirect without Location ('.$response->getStatusMessage().')');
}
private function isAuthenticatedRetryNeeded(array $job, Response $response)
{
if (in_array($response->getStatusCode(), array(401, 403)) && $job['attributes']['retryAuthFailure']) {
$warning = null;
if ($response->getHeader('content-type') === 'application/json') {
$data = json_decode($response->getBody(), true);
if (!empty($data['warning'])) {
$warning = $data['warning'];
}
}
$result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], $response->getStatusCode(), $response->getStatusMessage(), $warning, $response->getHeaders());
if ($result['retry']) {
return $result;
}
}
$locationHeader = $response->getHeader('location');
$needsAuthRetry = false;
// check for bitbucket login page asking to authenticate
if (
$job['origin'] === 'bitbucket.org'
&& !$this->authHelper->isPublicBitBucketDownload($job['url'])
&& substr($job['url'], -4) === '.zip'
&& (!$locationHeader || substr($locationHeader, -4) !== '.zip')
&& preg_match('{^text/html\b}i', $response->getHeader('content-type'))
) {
$needsAuthRetry = 'Bitbucket requires authentication and it was not provided';
}
// check for gitlab 404 when downloading archives
if (
$response->getStatusCode() === 404
&& $this->config && in_array($job['origin'], $this->config->get('gitlab-domains'), true)
&& false !== strpos($job['url'], 'archive.zip')
) {
$needsAuthRetry = 'GitLab requires authentication and it was not provided';
}
if ($needsAuthRetry) {
if ($job['attributes']['retryAuthFailure']) {
$result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], 401);
if ($result['retry']) {
return $result;
}
}
throw $this->failResponse($job, $response, $needsAuthRetry);
}
return array('retry' => false, 'storeAuth' => false);
}
private function restartJob(array $job, $url, array $attributes = array())
{
$attributes = array_merge($job['attributes'], $attributes);
$origin = Url::getOrigin($this->config, $url);
$this->initDownload($job['resolve'], $job['reject'], $origin, $url, $job['options'], $job['filename'], $attributes);
}
private function failResponse(array $job, Response $response, $errorMessage)
{
return new TransportException('The "'.$job['url'].'" file could not be downloaded ('.$errorMessage.')', $response->getStatusCode());
}
private function onProgress($curlHandle, callable $notify, array $progress, array $previousProgress)
{
// TODO add support for progress
if (300 <= $progress['http_code'] && $progress['http_code'] < 400) {
return;
}
if ($previousProgress['download_content_length'] < $progress['download_content_length']) {
$notify(STREAM_NOTIFY_FILE_SIZE_IS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, (int) $progress['download_content_length'], false);
}
if ($previousProgress['size_download'] < $progress['size_download']) {
$notify(STREAM_NOTIFY_PROGRESS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, (int) $progress['size_download'], (int) $progress['download_content_length'], false);
}
}
private function checkCurlResult($code)
{
if ($code != CURLM_OK && $code != CURLM_CALL_MULTI_PERFORM) {
throw new \RuntimeException(isset($this->multiErrors[$code])
? "cURL error: {$code} ({$this->multiErrors[$code][0]}): cURL message: {$this->multiErrors[$code][1]}"
: 'Unexpected cURL error: ' . $code
);
}
}
}

View File

@ -0,0 +1,94 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Util\Http;
use Composer\Json\JsonFile;
class Response
{
private $request;
private $code;
private $headers;
private $body;
public function __construct(array $request, $code, array $headers, $body)
{
if (!isset($request['url'])) {
throw new \LogicException('url key missing from request array');
}
$this->request = $request;
$this->code = (int) $code;
$this->headers = $headers;
$this->body = $body;
}
public function getStatusCode()
{
return $this->code;
}
/**
* @return string|null
*/
public function getStatusMessage()
{
$value = null;
foreach ($this->headers as $header) {
if (preg_match('{^HTTP/\S+ \d+}i', $header)) {
// In case of redirects, headers contain the headers of all responses
// so we can not return directly and need to keep iterating
$value = $header;
}
}
return $value;
}
public function getHeaders()
{
return $this->headers;
}
public function getHeader($name)
{
$value = null;
foreach ($this->headers as $header) {
if (preg_match('{^'.$name.':\s*(.+?)\s*$}i', $header, $match)) {
$value = $match[1];
} elseif (preg_match('{^HTTP/}i', $header)) {
// TODO ideally redirects would be handled in CurlDownloader/RemoteFilesystem and this becomes unnecessary
//
// In case of redirects, headers contains the headers of all responses
// so we reset the flag when a new response is being parsed as we are only interested in the last response
$value = null;
}
}
return $value;
}
public function getBody()
{
return $this->body;
}
public function decodeJson()
{
return JsonFile::parseJson($this->body, $this->request['url']);
}
public function collect()
{
$this->request = $this->code = $this->headers = $this->body = null;
}
}

View File

@ -0,0 +1,318 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Util;
use Composer\Config;
use Composer\IO\IOInterface;
use Composer\Downloader\TransportException;
use Composer\CaBundle\CaBundle;
use Composer\Util\Http\Response;
use Psr\Log\LoggerInterface;
use React\Promise\Promise;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class HttpDownloader
{
const STATUS_QUEUED = 1;
const STATUS_STARTED = 2;
const STATUS_COMPLETED = 3;
const STATUS_FAILED = 4;
private $io;
private $config;
private $jobs = array();
private $options = array();
private $runningJobs = 0;
private $maxJobs = 10;
private $lastProgress;
private $disableTls = false;
private $curl;
private $rfs;
private $idGen = 0;
private $disabled;
/**
* @param IOInterface $io The IO instance
* @param Config $config The config
* @param array $options The options
* @param bool $disableTls
*/
public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false)
{
$this->io = $io;
$this->disabled = (bool) getenv('COMPOSER_DISABLE_NETWORK');
// Setup TLS options
// The cafile option can be set via config.json
if ($disableTls === false) {
$logger = $io instanceof LoggerInterface ? $io : null;
$this->options = StreamContextFactory::getTlsDefaults($options, $logger);
} else {
$this->disableTls = true;
}
// handle the other externally set options normally.
$this->options = array_replace_recursive($this->options, $options);
$this->config = $config;
// TODO enable curl only on 5.6+ if older versions cause any problem
if (extension_loaded('curl')) {
$this->curl = new Http\CurlDownloader($io, $config, $options, $disableTls);
}
$this->rfs = new RemoteFilesystem($io, $config, $options, $disableTls);
}
public function get($url, $options = array())
{
list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => false), true);
$this->wait($job['id']);
return $this->getResponse($job['id']);
}
public function add($url, $options = array())
{
list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => false));
return $promise;
}
public function copy($url, $to, $options = array())
{
list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => $to), true);
$this->wait($job['id']);
return $this->getResponse($job['id']);
}
public function addCopy($url, $to, $options = array())
{
list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => $to));
return $promise;
}
/**
* Retrieve the options set in the constructor
*
* @return array Options
*/
public function getOptions()
{
return $this->options;
}
/**
* Merges new options
*
* @return array $options
*/
public function setOptions(array $options)
{
$this->options = array_replace_recursive($this->options, $options);
}
private function addJob($request, $sync = false)
{
$job = array(
'id' => $this->idGen++,
'status' => self::STATUS_QUEUED,
'request' => $request,
'sync' => $sync,
'origin' => Url::getOrigin($this->config, $request['url']),
);
// capture username/password from URL if there is one
if (preg_match('{^https?://([^:/]+):([^@/]+)@([^/]+)}i', $request['url'], $match)) {
$this->io->setAuthentication($job['origin'], rawurldecode($match[1]), rawurldecode($match[2]));
}
$rfs = $this->rfs;
if ($this->curl && preg_match('{^https?://}i', $job['request']['url'])) {
$resolver = function ($resolve, $reject) use (&$job) {
$job['status'] = HttpDownloader::STATUS_QUEUED;
$job['resolve'] = $resolve;
$job['reject'] = $reject;
};
} else {
$resolver = function ($resolve, $reject) use (&$job, $rfs) {
// start job
$url = $job['request']['url'];
$options = $job['request']['options'];
$job['status'] = HttpDownloader::STATUS_STARTED;
if ($job['request']['copyTo']) {
$result = $rfs->copy($job['origin'], $url, $job['request']['copyTo'], false /* TODO progress */, $options);
$headers = $rfs->getLastHeaders();
$response = new Http\Response($job['request'], $rfs->findStatusCode($headers), $headers, $job['request']['copyTo'].'~');
$resolve($response);
} else {
$body = $rfs->getContents($job['origin'], $url, false /* TODO progress */, $options);
$headers = $rfs->getLastHeaders();
$response = new Http\Response($job['request'], $rfs->findStatusCode($headers), $headers, $body);
$resolve($response);
}
};
}
$downloader = $this;
$io = $this->io;
$canceler = function () {};
$promise = new Promise($resolver, $canceler);
$promise->then(function ($response) use (&$job, $downloader) {
$job['status'] = HttpDownloader::STATUS_COMPLETED;
$job['response'] = $response;
// TODO 3.0 this should be done directly on $this when PHP 5.3 is dropped
$downloader->markJobDone();
$downloader->scheduleNextJob();
return $response;
}, function ($e) use ($io, &$job, $downloader) {
$job['status'] = HttpDownloader::STATUS_FAILED;
$job['exception'] = $e;
$downloader->markJobDone();
$downloader->scheduleNextJob();
throw $e;
});
$this->jobs[$job['id']] =& $job;
if ($this->runningJobs < $this->maxJobs) {
$this->startJob($job['id']);
}
return array($job, $promise);
}
private function startJob($id)
{
$job =& $this->jobs[$id];
if ($job['status'] !== self::STATUS_QUEUED) {
return;
}
// start job
$job['status'] = self::STATUS_STARTED;
$this->runningJobs++;
$resolve = $job['resolve'];
$reject = $job['reject'];
$url = $job['request']['url'];
$options = $job['request']['options'];
$origin = $job['origin'];
if ($this->disabled) {
if (isset($job['request']['options']['http']['header']) && false !== stripos(implode('', $job['request']['options']['http']['header']), 'if-modified-since')) {
$resolve(new Response(array('url' => $url), 304, array(), ''));
} else {
$e = new TransportException('Network disabled', 499);
$e->setStatusCode(499);
$reject($e);
}
return;
}
if ($job['request']['copyTo']) {
$this->curl->download($resolve, $reject, $origin, $url, $options, $job['request']['copyTo']);
} else {
$this->curl->download($resolve, $reject, $origin, $url, $options);
}
}
/**
* @private
*/
public function markJobDone()
{
$this->runningJobs--;
}
/**
* @private
*/
public function scheduleNextJob()
{
foreach ($this->jobs as $job) {
if ($job['status'] === self::STATUS_QUEUED) {
$this->startJob($job['id']);
if ($this->runningJobs >= $this->maxJobs) {
return;
}
}
}
}
public function wait($index = null, $progress = false)
{
while (true) {
if ($this->curl) {
$this->curl->tick();
}
if (null !== $index) {
if ($this->jobs[$index]['status'] === self::STATUS_COMPLETED || $this->jobs[$index]['status'] === self::STATUS_FAILED) {
return;
}
} else {
$done = true;
foreach ($this->jobs as $job) {
if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED), true)) {
$done = false;
break;
} elseif (!$job['sync']) {
unset($this->jobs[$job['id']]);
}
}
if ($done) {
return;
}
}
usleep(1000);
}
}
private function getResponse($index)
{
if (!isset($this->jobs[$index])) {
throw new \LogicException('Invalid request id');
}
if ($this->jobs[$index]['status'] === self::STATUS_FAILED) {
throw $this->jobs[$index]['exception'];
}
if (!isset($this->jobs[$index]['response'])) {
throw new \LogicException('Response not available yet, call wait() first');
}
$resp = $this->jobs[$index]['response'];
unset($this->jobs[$index]);
return $resp;
}
}

View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Util;
use Composer\Util\HttpDownloader;
use React\Promise\Promise;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class Loop
{
private $io;
public function __construct(HttpDownloader $httpDownloader)
{
$this->httpDownloader = $httpDownloader;
}
public function wait(array $promises)
{
$uncaught = null;
\React\Promise\all($promises)->then(
function () { },
function ($e) use (&$uncaught) {
$uncaught = $e;
}
);
$this->httpDownloader->wait();
if ($uncaught) {
throw $uncaught;
}
}
}

View File

@ -41,6 +41,7 @@ class RemoteFilesystem
private $retryAuthFailure;
private $lastHeaders;
private $storeAuth;
private $authHelper;
private $degradedMode = false;
private $redirects;
private $maxRedirects = 20;
@ -53,14 +54,15 @@ class RemoteFilesystem
* @param array $options The options
* @param bool $disableTls
*/
public function __construct(IOInterface $io, Config $config = null, array $options = array(), $disableTls = false)
public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false)
{
$this->io = $io;
// Setup TLS options
// The cafile option can be set via config.json
if ($disableTls === false) {
$this->options = $this->getTlsDefaults($options);
$logger = $io instanceof LoggerInterface ? $io : null;
$this->options = StreamContextFactory::getTlsDefaults($options, $logger);
} else {
$this->disableTls = true;
}
@ -68,6 +70,7 @@ class RemoteFilesystem
// handle the other externally set options normally.
$this->options = array_replace_recursive($this->options, $options);
$this->config = $config;
$this->authHelper = new AuthHelper($io, $config);
}
/**
@ -146,7 +149,7 @@ class RemoteFilesystem
* @param string $name header name (case insensitive)
* @return string|null
*/
public function findHeaderValue(array $headers, $name)
public static function findHeaderValue(array $headers, $name)
{
$value = null;
foreach ($headers as $header) {
@ -166,7 +169,7 @@ class RemoteFilesystem
* @param array $headers array of returned headers like from getLastHeaders()
* @return int|null
*/
public function findStatusCode(array $headers)
public static function findStatusCode(array $headers)
{
$value = null;
foreach ($headers as $header) {
@ -214,27 +217,6 @@ class RemoteFilesystem
*/
protected function get($originUrl, $fileUrl, $additionalOptions = array(), $fileName = null, $progress = true)
{
if (strpos($originUrl, '.github.com') === (strlen($originUrl) - 11)) {
$originUrl = 'github.com';
}
// Gitlab can be installed in a non-root context (i.e. gitlab.com/foo). When downloading archives the originUrl
// is the host without the path, so we look for the registered gitlab-domains matching the host here
if (
$this->config
&& is_array($this->config->get('gitlab-domains'))
&& false === strpos($originUrl, '/')
&& !in_array($originUrl, $this->config->get('gitlab-domains'))
) {
foreach ($this->config->get('gitlab-domains') as $gitlabDomain) {
if (0 === strpos($gitlabDomain, $originUrl)) {
$originUrl = $gitlabDomain;
break;
}
}
unset($gitlabDomain);
}
$this->scheme = parse_url($fileUrl, PHP_URL_SCHEME);
$this->bytesMax = 0;
$this->originUrl = $originUrl;
@ -246,11 +228,6 @@ class RemoteFilesystem
$this->lastHeaders = array();
$this->redirects = 1; // The first request counts.
// capture username/password from URL if there is one
if (preg_match('{^https?://([^:/]+):([^@/]+)@([^/]+)}i', $fileUrl, $match)) {
$this->io->setAuthentication($originUrl, rawurldecode($match[1]), rawurldecode($match[2]));
}
$tempAdditionalOptions = $additionalOptions;
if (isset($tempAdditionalOptions['retry-auth-failure'])) {
$this->retryAuthFailure = (bool) $tempAdditionalOptions['retry-auth-failure'];
@ -271,14 +248,6 @@ class RemoteFilesystem
$origFileUrl = $fileUrl;
if (isset($options['github-token'])) {
// only add the access_token if it is actually a github URL (in case we were redirected to S3)
if (preg_match('{^https?://([a-z0-9-]+\.)*github\.com/}', $fileUrl)) {
$fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['github-token'];
}
unset($options['github-token']);
}
if (isset($options['gitlab-token'])) {
$fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['gitlab-token'];
unset($options['gitlab-token']);
@ -399,7 +368,7 @@ class RemoteFilesystem
// check for bitbucket login page asking to authenticate
if ($originUrl === 'bitbucket.org'
&& !$this->isPublicBitBucketDownload($fileUrl)
&& !$this->authHelper->isPublicBitBucketDownload($fileUrl)
&& substr($fileUrl, -4) === '.zip'
&& (!$locationHeader || substr($locationHeader, -4) !== '.zip')
&& $contentType && preg_match('{^text/html\b}i', $contentType)
@ -543,8 +512,7 @@ class RemoteFilesystem
$result = $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress);
if ($this->storeAuth && $this->config) {
$authHelper = new AuthHelper($this->io, $this->config);
$authHelper->storeAuth($this->originUrl, $this->storeAuth);
$this->authHelper->storeAuth($this->originUrl, $this->storeAuth);
$this->storeAuth = false;
}
@ -649,111 +617,14 @@ class RemoteFilesystem
protected function promptAuthAndRetry($httpStatus, $reason = null, $warning = null, $headers = array())
{
if ($this->config && in_array($this->originUrl, $this->config->get('github-domains'), true)) {
$gitHubUtil = new GitHub($this->io, $this->config, null);
$message = "\n";
$result = $this->authHelper->promptAuthIfNeeded($this->fileUrl, $this->originUrl, $httpStatus, $reason, $warning, $headers);
$rateLimited = $gitHubUtil->isRateLimited($headers);
if ($rateLimited) {
$rateLimit = $gitHubUtil->getRateLimit($headers);
if ($this->io->hasAuthentication($this->originUrl)) {
$message = 'Review your configured GitHub OAuth token or enter a new one to go over the API rate limit.';
} else {
$message = 'Create a GitHub OAuth token to go over the API rate limit.';
}
$this->storeAuth = $result['storeAuth'];
$this->retry = $result['retry'];
$message = sprintf(
'GitHub API limit (%d calls/hr) is exhausted, could not fetch '.$this->fileUrl.'. '.$message.' You can also wait until %s for the rate limit to reset.',
$rateLimit['limit'],
$rateLimit['reset']
)."\n";
} else {
$message .= 'Could not fetch '.$this->fileUrl.', please ';
if ($this->io->hasAuthentication($this->originUrl)) {
$message .= 'review your configured GitHub OAuth token or enter a new one to access private repos';
} else {
$message .= 'create a GitHub OAuth token to access private repos';
}
}
if (!$gitHubUtil->authorizeOAuth($this->originUrl)
&& (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($this->originUrl, $message))
) {
throw new TransportException('Could not authenticate against '.$this->originUrl, 401);
}
} elseif ($this->config && in_array($this->originUrl, $this->config->get('gitlab-domains'), true)) {
$message = "\n".'Could not fetch '.$this->fileUrl.', enter your ' . $this->originUrl . ' credentials ' .($httpStatus === 401 ? 'to access private repos' : 'to go over the API rate limit');
$gitLabUtil = new GitLab($this->io, $this->config, null);
if ($this->io->hasAuthentication($this->originUrl) && ($auth = $this->io->getAuthentication($this->originUrl)) && $auth['password'] === 'private-token') {
throw new TransportException("Invalid credentials for '" . $this->fileUrl . "', aborting.", $httpStatus);
}
if (!$gitLabUtil->authorizeOAuth($this->originUrl)
&& (!$this->io->isInteractive() || !$gitLabUtil->authorizeOAuthInteractively($this->scheme, $this->originUrl, $message))
) {
throw new TransportException('Could not authenticate against '.$this->originUrl, 401);
}
} elseif ($this->config && $this->originUrl === 'bitbucket.org') {
$askForOAuthToken = true;
if ($this->io->hasAuthentication($this->originUrl)) {
$auth = $this->io->getAuthentication($this->originUrl);
if ($auth['username'] !== 'x-token-auth') {
$bitbucketUtil = new Bitbucket($this->io, $this->config);
$accessToken = $bitbucketUtil->requestToken($this->originUrl, $auth['username'], $auth['password']);
if (!empty($accessToken)) {
$this->io->setAuthentication($this->originUrl, 'x-token-auth', $accessToken);
$askForOAuthToken = false;
}
} else {
throw new TransportException('Could not authenticate against ' . $this->originUrl, 401);
}
}
if ($askForOAuthToken) {
$message = "\n".'Could not fetch ' . $this->fileUrl . ', please create a bitbucket OAuth token to ' . (($httpStatus === 401 || $httpStatus === 403) ? 'access private repos' : 'go over the API rate limit');
$bitBucketUtil = new Bitbucket($this->io, $this->config);
if (! $bitBucketUtil->authorizeOAuth($this->originUrl)
&& (! $this->io->isInteractive() || !$bitBucketUtil->authorizeOAuthInteractively($this->originUrl, $message))
) {
throw new TransportException('Could not authenticate against ' . $this->originUrl, 401);
}
}
} else {
// 404s are only handled for github
if ($httpStatus === 404) {
return;
}
// fail if the console is not interactive
if (!$this->io->isInteractive()) {
if ($httpStatus === 401) {
$message = "The '" . $this->fileUrl . "' URL required authentication.\nYou must be using the interactive console to authenticate";
}
if ($httpStatus === 403) {
$message = "The '" . $this->fileUrl . "' URL could not be accessed: " . $reason;
}
throw new TransportException($message, $httpStatus);
}
// fail if we already have auth
if ($this->io->hasAuthentication($this->originUrl)) {
throw new TransportException("Invalid credentials for '" . $this->fileUrl . "', aborting.", $httpStatus);
}
$this->io->overwriteError('');
if ($warning) {
$this->io->writeError(' <warning>'.$warning.'</warning>');
}
$this->io->writeError(' Authentication required (<info>'.parse_url($this->fileUrl, PHP_URL_HOST).'</info>):');
$username = $this->io->ask(' Username: ');
$password = $this->io->askAndHideAnswer(' Password: ');
$this->io->setAuthentication($this->originUrl, $username, $password);
$this->storeAuth = $this->config->get('store-auths');
if ($this->retry) {
throw new TransportException('RETRY');
}
$this->retry = true;
throw new TransportException('RETRY');
}
protected function getOptionsForUrl($originUrl, $additionalOptions)
@ -813,27 +684,7 @@ class RemoteFilesystem
$headers[] = 'Connection: close';
}
if ($this->io->hasAuthentication($originUrl)) {
$auth = $this->io->getAuthentication($originUrl);
if ('github.com' === $originUrl && 'x-oauth-basic' === $auth['password']) {
$options['github-token'] = $auth['username'];
} elseif ($this->config && in_array($originUrl, $this->config->get('gitlab-domains'), true)) {
if ($auth['password'] === 'oauth2') {
$headers[] = 'Authorization: Bearer '.$auth['username'];
} elseif ($auth['password'] === 'private-token') {
$headers[] = 'PRIVATE-TOKEN: '.$auth['username'];
}
} elseif ('bitbucket.org' === $originUrl
&& $this->fileUrl !== Bitbucket::OAUTH2_ACCESS_TOKEN_URL && 'x-token-auth' === $auth['username']
) {
if (!$this->isPublicBitBucketDownload($this->fileUrl)) {
$headers[] = 'Authorization: Bearer ' . $auth['password'];
}
} else {
$authStr = base64_encode($auth['username'] . ':' . $auth['password']);
$headers[] = 'Authorization: Basic '.$authStr;
}
}
$headers = $this->authHelper->addAuthenticationHeader($headers, $originUrl, $this->fileUrl);
$options['http']['follow_location'] = 0;
@ -891,111 +742,6 @@ class RemoteFilesystem
return false;
}
/**
* @param array $options
*
* @return array
*/
private function getTlsDefaults(array $options)
{
$ciphers = implode(':', array(
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'DHE-RSA-AES128-GCM-SHA256',
'DHE-DSS-AES128-GCM-SHA256',
'kEDH+AESGCM',
'ECDHE-RSA-AES128-SHA256',
'ECDHE-ECDSA-AES128-SHA256',
'ECDHE-RSA-AES128-SHA',
'ECDHE-ECDSA-AES128-SHA',
'ECDHE-RSA-AES256-SHA384',
'ECDHE-ECDSA-AES256-SHA384',
'ECDHE-RSA-AES256-SHA',
'ECDHE-ECDSA-AES256-SHA',
'DHE-RSA-AES128-SHA256',
'DHE-RSA-AES128-SHA',
'DHE-DSS-AES128-SHA256',
'DHE-RSA-AES256-SHA256',
'DHE-DSS-AES256-SHA',
'DHE-RSA-AES256-SHA',
'AES128-GCM-SHA256',
'AES256-GCM-SHA384',
'AES128-SHA256',
'AES256-SHA256',
'AES128-SHA',
'AES256-SHA',
'AES',
'CAMELLIA',
'DES-CBC3-SHA',
'!aNULL',
'!eNULL',
'!EXPORT',
'!DES',
'!RC4',
'!MD5',
'!PSK',
'!aECDH',
'!EDH-DSS-DES-CBC3-SHA',
'!EDH-RSA-DES-CBC3-SHA',
'!KRB5-DES-CBC3-SHA',
));
/**
* CN_match and SNI_server_name are only known once a URL is passed.
* They will be set in the getOptionsForUrl() method which receives a URL.
*
* cafile or capath can be overridden by passing in those options to constructor.
*/
$defaults = array(
'ssl' => array(
'ciphers' => $ciphers,
'verify_peer' => true,
'verify_depth' => 7,
'SNI_enabled' => true,
'capture_peer_cert' => true,
),
);
if (isset($options['ssl'])) {
$defaults['ssl'] = array_replace_recursive($defaults['ssl'], $options['ssl']);
}
$caBundleLogger = $this->io instanceof LoggerInterface ? $this->io : null;
/**
* Attempt to find a local cafile or throw an exception if none pre-set
* The user may go download one if this occurs.
*/
if (!isset($defaults['ssl']['cafile']) && !isset($defaults['ssl']['capath'])) {
$result = CaBundle::getSystemCaRootBundlePath($caBundleLogger);
if (is_dir($result)) {
$defaults['ssl']['capath'] = $result;
} else {
$defaults['ssl']['cafile'] = $result;
}
}
if (isset($defaults['ssl']['cafile']) && (!is_readable($defaults['ssl']['cafile']) || !CaBundle::validateCaFile($defaults['ssl']['cafile'], $caBundleLogger))) {
throw new TransportException('The configured cafile was not valid or could not be read.');
}
if (isset($defaults['ssl']['capath']) && (!is_dir($defaults['ssl']['capath']) || !is_readable($defaults['ssl']['capath']))) {
throw new TransportException('The configured capath was not valid or could not be read.');
}
/**
* Disable TLS compression to prevent CRIME attacks where supported.
*/
if (PHP_VERSION_ID >= 50413) {
$defaults['ssl']['disable_compression'] = true;
}
return $defaults;
}
/**
* Fetch certificate common name and fingerprint for validation of SAN.
*
@ -1065,29 +811,4 @@ class RemoteFilesystem
return parse_url($url, PHP_URL_HOST).':'.$port;
}
/**
* @link https://github.com/composer/composer/issues/5584
*
* @param string $urlToBitBucketFile URL to a file at bitbucket.org.
*
* @return bool Whether the given URL is a public BitBucket download which requires no authentication.
*/
private function isPublicBitBucketDownload($urlToBitBucketFile)
{
$domain = parse_url($urlToBitBucketFile, PHP_URL_HOST);
if (strpos($domain, 'bitbucket.org') === false) {
// Bitbucket downloads are hosted on amazonaws.
// We do not need to authenticate there at all
return true;
}
$path = parse_url($urlToBitBucketFile, PHP_URL_PATH);
// Path for a public download follows this pattern /{user}/{repo}/downloads/{whatever}
// {@link https://blog.bitbucket.org/2009/04/12/new-feature-downloads/}
$pathParts = explode('/', $path);
return count($pathParts) >= 4 && $pathParts[3] == 'downloads';
}
}

View File

@ -13,6 +13,8 @@
namespace Composer\Util;
use Composer\Composer;
use Composer\CaBundle\CaBundle;
use Psr\Log\LoggerInterface;
/**
* Allows the creation of a basic context supporting http proxy
@ -39,6 +41,32 @@ final class StreamContextFactory
'max_redirects' => 20,
));
$options = array_replace_recursive($options, self::initOptions($url, $defaultOptions));
unset($defaultOptions['http']['header']);
$options = array_replace_recursive($options, $defaultOptions);
if (isset($options['http']['header'])) {
$options['http']['header'] = self::fixHttpHeaderField($options['http']['header']);
}
return stream_context_create($options, $defaultParams);
}
/**
* @param string $url
* @param array $options
* @return array ['http' => ['header' => [...], 'proxy' => '..', 'request_fulluri' => bool]] formatted as a stream context array
*/
public static function initOptions($url, array $options)
{
// Make sure the headers are in an array form
if (!isset($options['http']['header'])) {
$options['http']['header'] = array();
}
if (is_string($options['http']['header'])) {
$options['http']['header'] = explode("\r\n", $options['http']['header']);
}
// Handle HTTP_PROXY/http_proxy on CLI only for security reasons
if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && (!empty($_SERVER['HTTP_PROXY']) || !empty($_SERVER['http_proxy']))) {
$proxy = parse_url(!empty($_SERVER['http_proxy']) ? $_SERVER['http_proxy'] : $_SERVER['HTTP_PROXY']);
@ -85,15 +113,15 @@ final class StreamContextFactory
// enabled request_fulluri unless it is explicitly disabled
switch (parse_url($url, PHP_URL_SCHEME)) {
case 'http': // default request_fulluri to true
case 'http': // default request_fulluri to true for HTTP
$reqFullUriEnv = getenv('HTTP_PROXY_REQUEST_FULLURI');
if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) {
$options['http']['request_fulluri'] = true;
}
break;
case 'https': // default request_fulluri to true
case 'https': // default request_fulluri to false for HTTPS
$reqFullUriEnv = getenv('HTTPS_PROXY_REQUEST_FULLURI');
if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) {
if (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv) {
$options['http']['request_fulluri'] = true;
}
break;
@ -115,42 +143,139 @@ final class StreamContextFactory
}
$auth = base64_encode($auth);
// Preserve headers if already set in default options
if (isset($defaultOptions['http']['header'])) {
if (is_string($defaultOptions['http']['header'])) {
$defaultOptions['http']['header'] = array($defaultOptions['http']['header']);
}
$defaultOptions['http']['header'][] = "Proxy-Authorization: Basic {$auth}";
} else {
$options['http']['header'] = array("Proxy-Authorization: Basic {$auth}");
}
$options['http']['header'][] = "Proxy-Authorization: Basic {$auth}";
}
}
$options = array_replace_recursive($options, $defaultOptions);
if (isset($options['http']['header'])) {
$options['http']['header'] = self::fixHttpHeaderField($options['http']['header']);
}
if (defined('HHVM_VERSION')) {
$phpVersion = 'HHVM ' . HHVM_VERSION;
} else {
$phpVersion = 'PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION;
}
if (extension_loaded('curl')) {
$curl = curl_version();
$httpVersion = 'curl '.$curl['version'];
} else {
$httpVersion = 'streams';
}
if (!isset($options['http']['header']) || false === stripos(implode('', $options['http']['header']), 'user-agent')) {
$options['http']['header'][] = sprintf(
'User-Agent: Composer/%s (%s; %s; %s%s)',
Composer::VERSION === '@package_version@' ? 'source' : Composer::VERSION,
'User-Agent: Composer/%s (%s; %s; %s; %s%s)',
Composer::VERSION === '@package_version@' ? Composer::SOURCE_VERSION : Composer::VERSION,
function_exists('php_uname') ? php_uname('s') : 'Unknown',
function_exists('php_uname') ? php_uname('r') : 'Unknown',
$phpVersion,
$httpVersion,
getenv('CI') ? '; CI' : ''
);
}
return stream_context_create($options, $defaultParams);
return $options;
}
/**
* @param array $options
*
* @return array
*/
public static function getTlsDefaults(array $options, LoggerInterface $logger = null)
{
$ciphers = implode(':', array(
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'DHE-RSA-AES128-GCM-SHA256',
'DHE-DSS-AES128-GCM-SHA256',
'kEDH+AESGCM',
'ECDHE-RSA-AES128-SHA256',
'ECDHE-ECDSA-AES128-SHA256',
'ECDHE-RSA-AES128-SHA',
'ECDHE-ECDSA-AES128-SHA',
'ECDHE-RSA-AES256-SHA384',
'ECDHE-ECDSA-AES256-SHA384',
'ECDHE-RSA-AES256-SHA',
'ECDHE-ECDSA-AES256-SHA',
'DHE-RSA-AES128-SHA256',
'DHE-RSA-AES128-SHA',
'DHE-DSS-AES128-SHA256',
'DHE-RSA-AES256-SHA256',
'DHE-DSS-AES256-SHA',
'DHE-RSA-AES256-SHA',
'AES128-GCM-SHA256',
'AES256-GCM-SHA384',
'AES128-SHA256',
'AES256-SHA256',
'AES128-SHA',
'AES256-SHA',
'AES',
'CAMELLIA',
'DES-CBC3-SHA',
'!aNULL',
'!eNULL',
'!EXPORT',
'!DES',
'!RC4',
'!MD5',
'!PSK',
'!aECDH',
'!EDH-DSS-DES-CBC3-SHA',
'!EDH-RSA-DES-CBC3-SHA',
'!KRB5-DES-CBC3-SHA',
));
/**
* CN_match and SNI_server_name are only known once a URL is passed.
* They will be set in the getOptionsForUrl() method which receives a URL.
*
* cafile or capath can be overridden by passing in those options to constructor.
*/
$defaults = array(
'ssl' => array(
'ciphers' => $ciphers,
'verify_peer' => true,
'verify_depth' => 7,
'SNI_enabled' => true,
'capture_peer_cert' => true,
),
);
if (isset($options['ssl'])) {
$defaults['ssl'] = array_replace_recursive($defaults['ssl'], $options['ssl']);
}
/**
* Attempt to find a local cafile or throw an exception if none pre-set
* The user may go download one if this occurs.
*/
if (!isset($defaults['ssl']['cafile']) && !isset($defaults['ssl']['capath'])) {
$result = CaBundle::getSystemCaRootBundlePath($logger);
if (is_dir($result)) {
$defaults['ssl']['capath'] = $result;
} else {
$defaults['ssl']['cafile'] = $result;
}
}
if (isset($defaults['ssl']['cafile']) && (!is_readable($defaults['ssl']['cafile']) || !CaBundle::validateCaFile($defaults['ssl']['cafile'], $logger))) {
throw new TransportException('The configured cafile was not valid or could not be read.');
}
if (isset($defaults['ssl']['capath']) && (!is_dir($defaults['ssl']['capath']) || !is_readable($defaults['ssl']['capath']))) {
throw new TransportException('The configured capath was not valid or could not be read.');
}
/**
* Disable TLS compression to prevent CRIME attacks where supported.
*/
if (PHP_VERSION_ID >= 50413) {
$defaults['ssl']['disable_compression'] = true;
}
return $defaults;
}
/**

View File

@ -19,6 +19,12 @@ use Composer\Config;
*/
class Url
{
/**
* @param Config $config
* @param string $url
* @param string $ref
* @return string the updated URL
*/
public static function updateDistReference(Config $config, $url, $ref)
{
$host = parse_url($url, PHP_URL_HOST);
@ -52,4 +58,45 @@ class Url
return $url;
}
/**
* @param string $url
* @return string
*/
public static function getOrigin(Config $config, $url)
{
if (0 === strpos($url, 'file://')) {
return $url;
}
$origin = (string) parse_url($url, PHP_URL_HOST);
if (strpos($origin, '.github.com') === (strlen($origin) - 11)) {
return 'github.com';
}
if ($origin === 'repo.packagist.org') {
return 'packagist.org';
}
if ($origin === '') {
$origin = $url;
}
// Gitlab can be installed in a non-root context (i.e. gitlab.com/foo). When downloading archives the originUrl
// is the host without the path, so we look for the registered gitlab-domains matching the host here
if (
is_array($config->get('gitlab-domains'))
&& false === strpos($origin, '/')
&& !in_array($origin, $config->get('gitlab-domains'))
) {
foreach ($config->get('gitlab-domains') as $gitlabDomain) {
if (0 === strpos($gitlabDomain, $origin)) {
return $gitlabDomain;
}
}
}
return $origin;
}
}

View File

@ -57,7 +57,7 @@ class ComposerTest extends TestCase
public function testSetGetInstallationManager()
{
$composer = new Composer();
$manager = $this->getMockBuilder('Composer\Installer\InstallationManager')->getMock();
$manager = $this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock();
$composer->setInstallationManager($manager);
$this->assertSame($manager, $composer->getInstallationManager());

View File

@ -29,7 +29,7 @@ class ArchiveDownloaderTest extends TestCase
$method->setAccessible(true);
$first = $method->invoke($downloader, $packageMock, '/path');
$this->assertRegExp('#/path/[a-z0-9]+\.js#', $first);
$this->assertRegExp('#/path_[a-z0-9]+\.js#', $first);
$this->assertSame($first, $method->invoke($downloader, $packageMock, '/path'));
}
@ -156,7 +156,11 @@ class ArchiveDownloaderTest extends TestCase
{
return $this->getMockForAbstractClass(
'Composer\Downloader\ArchiveDownloader',
array($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getMockBuilder('Composer\Config')->getMock())
array(
$io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(),
$config = $this->getMockBuilder('Composer\Config')->getMock(),
new \Composer\Util\HttpDownloader($io, $config),
)
);
}
}

View File

@ -50,7 +50,7 @@ class DownloadManagerTest extends TestCase
$this->setExpectedException('InvalidArgumentException');
$manager->getDownloaderForInstalledPackage($package);
$manager->getDownloaderForPackage($package);
}
public function testGetDownloaderForCorrectlyInstalledDistPackage()
@ -82,7 +82,7 @@ class DownloadManagerTest extends TestCase
->with('pear')
->will($this->returnValue($downloader));
$this->assertSame($downloader, $manager->getDownloaderForInstalledPackage($package));
$this->assertSame($downloader, $manager->getDownloaderForPackage($package));
}
public function testGetDownloaderForIncorrectlyInstalledDistPackage()
@ -116,7 +116,7 @@ class DownloadManagerTest extends TestCase
$this->setExpectedException('LogicException');
$manager->getDownloaderForInstalledPackage($package);
$manager->getDownloaderForPackage($package);
}
public function testGetDownloaderForCorrectlyInstalledSourcePackage()
@ -148,7 +148,7 @@ class DownloadManagerTest extends TestCase
->with('git')
->will($this->returnValue($downloader));
$this->assertSame($downloader, $manager->getDownloaderForInstalledPackage($package));
$this->assertSame($downloader, $manager->getDownloaderForPackage($package));
}
public function testGetDownloaderForIncorrectlyInstalledSourcePackage()
@ -182,7 +182,7 @@ class DownloadManagerTest extends TestCase
$this->setExpectedException('LogicException');
$manager->getDownloaderForInstalledPackage($package);
$manager->getDownloaderForPackage($package);
}
public function testGetDownloaderForMetapackage()
@ -195,7 +195,7 @@ class DownloadManagerTest extends TestCase
$manager = new DownloadManager($this->io, false, $this->filesystem);
$this->assertNull($manager->getDownloaderForInstalledPackage($package));
$this->assertNull($manager->getDownloaderForPackage($package));
}
public function testFullPackageDownload()
@ -223,11 +223,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue($downloader));
@ -274,16 +274,16 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->at(0))
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue($downloaderFail));
$manager
->expects($this->at(1))
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue($downloaderSuccess));
@ -333,11 +333,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue($downloader));
@ -369,11 +369,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue($downloader));
@ -399,11 +399,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue(null)); // There is no downloader for Metapackages.
@ -435,11 +435,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue($downloader));
@ -472,11 +472,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue($downloader));
@ -509,11 +509,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue($downloader));
@ -550,33 +550,30 @@ class DownloadManagerTest extends TestCase
$initial
->expects($this->once())
->method('getDistType')
->will($this->returnValue('pear'));
->will($this->returnValue('zip'));
$target = $this->createPackageMock();
$target
->expects($this->once())
->method('getDistType')
->will($this->returnValue('pear'));
->method('getInstallationSource')
->will($this->returnValue('dist'));
$target
->expects($this->once())
->method('setInstallationSource')
->with('dist');
->method('getDistType')
->will($this->returnValue('zip'));
$pearDownloader = $this->createDownloaderMock();
$pearDownloader
$zipDownloader = $this->createDownloaderMock();
$zipDownloader
->expects($this->once())
->method('update')
->with($initial, $target, 'vendor/bundles/FOS/UserBundle');
$zipDownloader
->expects($this->any())
->method('getInstallationSource')
->will($this->returnValue('dist'));
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->with($initial)
->will($this->returnValue($pearDownloader));
$manager = new DownloadManager($this->io, false, $this->filesystem);
$manager->setDownloader('zip', $zipDownloader);
$manager->update($initial, $target, 'vendor/bundles/FOS/UserBundle');
}
@ -591,113 +588,89 @@ class DownloadManagerTest extends TestCase
$initial
->expects($this->once())
->method('getDistType')
->will($this->returnValue('pear'));
->will($this->returnValue('xz'));
$target = $this->createPackageMock();
$target
->expects($this->once())
->expects($this->any())
->method('getInstallationSource')
->will($this->returnValue('dist'));
$target
->expects($this->any())
->method('getDistType')
->will($this->returnValue('composer'));
->will($this->returnValue('zip'));
$pearDownloader = $this->createDownloaderMock();
$pearDownloader
$xzDownloader = $this->createDownloaderMock();
$xzDownloader
->expects($this->once())
->method('remove')
->with($initial, 'vendor/bundles/FOS/UserBundle');
$xzDownloader
->expects($this->any())
->method('getInstallationSource')
->will($this->returnValue('dist'));
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage', 'download'))
->getMock();
$manager
$zipDownloader = $this->createDownloaderMock();
$zipDownloader
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->with($initial)
->will($this->returnValue($pearDownloader));
$manager
->expects($this->once())
->method('download')
->with($target, 'vendor/bundles/FOS/UserBundle', false);
->method('install')
->with($target, 'vendor/bundles/FOS/UserBundle');
$zipDownloader
->expects($this->any())
->method('getInstallationSource')
->will($this->returnValue('dist'));
$manager = new DownloadManager($this->io, false, $this->filesystem);
$manager->setDownloader('xz', $xzDownloader);
$manager->setDownloader('zip', $zipDownloader);
$manager->update($initial, $target, 'vendor/bundles/FOS/UserBundle');
}
public function testUpdateSourceWithEqualTypes()
/**
* @dataProvider updatesProvider
*/
public function testGetAvailableSourcesUpdateSticksToSameSource($prevPkgSource, $prevPkgIsDev, $targetAvailable, $targetIsDev, $expected)
{
$initial = $this->createPackageMock();
$initial
->expects($this->once())
->method('getInstallationSource')
->will($this->returnValue('source'));
$initial
->expects($this->once())
->method('getSourceType')
->will($this->returnValue('svn'));
$initial = null;
if ($prevPkgSource) {
$initial = $this->prophesize('Composer\Package\PackageInterface');
$initial->getInstallationSource()->willReturn($prevPkgSource);
$initial->isDev()->willReturn($prevPkgIsDev);
}
$target = $this->createPackageMock();
$target
->expects($this->once())
->method('getSourceType')
->will($this->returnValue('svn'));
$target = $this->prophesize('Composer\Package\PackageInterface');
$target->getSourceType()->willReturn(in_array('source', $targetAvailable, true) ? 'git' : null);
$target->getDistType()->willReturn(in_array('dist', $targetAvailable, true) ? 'zip' : null);
$target->isDev()->willReturn($targetIsDev);
$svnDownloader = $this->createDownloaderMock();
$svnDownloader
->expects($this->once())
->method('update')
->with($initial, $target, 'vendor/pkg');
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage', 'download'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->with($initial)
->will($this->returnValue($svnDownloader));
$manager->update($initial, $target, 'vendor/pkg');
$manager = new DownloadManager($this->io, false, $this->filesystem);
$method = new \ReflectionMethod($manager, 'getAvailableSources');
$method->setAccessible(true);
$this->assertEquals($expected, $method->invoke($manager, $target->reveal(), $initial ? $initial->reveal() : null));
}
public function testUpdateSourceWithNotEqualTypes()
public static function updatesProvider()
{
$initial = $this->createPackageMock();
$initial
->expects($this->once())
->method('getInstallationSource')
->will($this->returnValue('source'));
$initial
->expects($this->once())
->method('getSourceType')
->will($this->returnValue('svn'));
$target = $this->createPackageMock();
$target
->expects($this->once())
->method('getSourceType')
->will($this->returnValue('git'));
$svnDownloader = $this->createDownloaderMock();
$svnDownloader
->expects($this->once())
->method('remove')
->with($initial, 'vendor/pkg');
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage', 'download'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->with($initial)
->will($this->returnValue($svnDownloader));
$manager
->expects($this->once())
->method('download')
->with($target, 'vendor/pkg', true);
$manager->update($initial, $target, 'vendor/pkg');
return array(
// prevPkg source, prevPkg isDev, pkg available, pkg isDev, expected
// updates keep previous source as preference
array('source', false, array('source', 'dist'), false, array('source', 'dist')),
array('dist', false, array('source', 'dist'), false, array('dist', 'source')),
// updates do not keep previous source if target package does not have it
array('source', false, array('dist'), false, array('dist')),
array('dist', false, array('source'), false, array('source')),
// updates do not keep previous source if target is dev and prev wasn't dev and installed from dist
array('source', false, array('source', 'dist'), true, array('source', 'dist')),
array('dist', false, array('source', 'dist'), true, array('source', 'dist')),
// install picks the right default
array(null, null, array('source', 'dist'), true, array('source', 'dist')),
array(null, null, array('dist'), true, array('dist')),
array(null, null, array('source'), true, array('source')),
array(null, null, array('source', 'dist'), false, array('dist', 'source')),
array(null, null, array('dist'), false, array('dist')),
array(null, null, array('source'), false, array('source')),
);
}
public function testUpdateMetapackage()
@ -707,11 +680,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->expects($this->exactly(2))
->method('getDownloaderForPackage')
->with($initial)
->will($this->returnValue(null)); // There is no downloader for metapackages.
@ -730,11 +703,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue($pearDownloader));
@ -747,11 +720,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue(null)); // There is no downloader for metapackages.
@ -790,11 +763,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue($downloader));
@ -833,11 +806,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue($downloader));
@ -879,11 +852,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue($downloader));
$manager->setPreferences(array('foo/*' => 'source'));
@ -926,11 +899,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue($downloader));
$manager->setPreferences(array('foo/*' => 'source'));
@ -973,11 +946,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue($downloader));
$manager->setPreferences(array('foo/*' => 'auto'));
@ -1020,11 +993,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue($downloader));
$manager->setPreferences(array('foo/*' => 'auto'));
@ -1063,11 +1036,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue($downloader));
$manager->setPreferences(array('foo/*' => 'source'));
@ -1106,11 +1079,11 @@ class DownloadManagerTest extends TestCase
$manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
->setConstructorArgs(array($this->io, false, $this->filesystem))
->setMethods(array('getDownloaderForInstalledPackage'))
->setMethods(array('getDownloaderForPackage'))
->getMock();
$manager
->expects($this->once())
->method('getDownloaderForInstalledPackage')
->method('getDownloaderForPackage')
->with($package)
->will($this->returnValue($downloader));
$manager->setPreferences(array('foo/*' => 'dist'));

View File

@ -15,16 +15,23 @@ namespace Composer\Test\Downloader;
use Composer\Downloader\FileDownloader;
use Composer\Test\TestCase;
use Composer\Util\Filesystem;
use Composer\Util\Http\Response;
use Composer\Util\Loop;
class FileDownloaderTest extends TestCase
{
protected function getDownloader($io = null, $config = null, $eventDispatcher = null, $cache = null, $rfs = null, $filesystem = null)
protected function getDownloader($io = null, $config = null, $eventDispatcher = null, $cache = null, $httpDownloader = null, $filesystem = null)
{
$io = $io ?: $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
$config = $config ?: $this->getMockBuilder('Composer\Config')->getMock();
$rfs = $rfs ?: $this->getMockBuilder('Composer\Util\RemoteFilesystem')->disableOriginalConstructor()->getMock();
$httpDownloader = $httpDownloader ?: $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock();
$httpDownloader
->expects($this->any())
->method('addCopy')
->will($this->returnValue(\React\Promise\resolve(new Response(array('url' => 'http://example.org/'), 200, array(), 'file~'))));
$this->httpDownloader = $httpDownloader;
return new FileDownloader($io, $config, $eventDispatcher, $cache, $rfs, $filesystem);
return new FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $filesystem);
}
/**
@ -84,7 +91,7 @@ class FileDownloaderTest extends TestCase
$method = new \ReflectionMethod($downloader, 'getFileName');
$method->setAccessible(true);
$this->assertEquals('/path/script.js', $method->invoke($downloader, $packageMock, '/path'));
$this->assertEquals('/path_script.js', $method->invoke($downloader, $packageMock, '/path'));
}
public function testDownloadButFileIsUnsaved()
@ -118,8 +125,11 @@ class FileDownloaderTest extends TestCase
$downloader = $this->getDownloader($ioMock);
try {
$downloader->download($packageMock, $path);
$this->fail();
$promise = $downloader->download($packageMock, $path);
$loop = new Loop($this->httpDownloader);
$loop->wait(array($promise));
$this->fail('Download was expected to throw');
} catch (\Exception $e) {
if (is_dir($path)) {
$fs = new Filesystem();
@ -128,7 +138,7 @@ class FileDownloaderTest extends TestCase
unlink($path);
}
$this->assertInstanceOf('UnexpectedValueException', $e);
$this->assertInstanceOf('UnexpectedValueException', $e, $e->getMessage());
$this->assertContains('could not be saved to', $e->getMessage());
}
}
@ -188,11 +198,14 @@ class FileDownloaderTest extends TestCase
$path = $this->getUniqueTmpDirectory();
$downloader = $this->getDownloader(null, null, null, null, null, $filesystem);
// make sure the file expected to be downloaded is on disk already
touch($path.'/script.js');
touch($path.'_script.js');
try {
$downloader->download($packageMock, $path);
$this->fail();
$promise = $downloader->download($packageMock, $path);
$loop = new Loop($this->httpDownloader);
$loop->wait(array($promise));
$this->fail('Download was expected to throw');
} catch (\Exception $e) {
if (is_dir($path)) {
$fs = new Filesystem();
@ -201,7 +214,7 @@ class FileDownloaderTest extends TestCase
unlink($path);
}
$this->assertInstanceOf('UnexpectedValueException', $e);
$this->assertInstanceOf('UnexpectedValueException', $e, $e->getMessage());
$this->assertContains('checksum verification', $e->getMessage());
}
}
@ -232,17 +245,25 @@ class FileDownloaderTest extends TestCase
$ioMock = $this->getMock('Composer\IO\IOInterface');
$ioMock->expects($this->at(0))
->method('writeError')
->with($this->stringContains('Downloading'));
$ioMock->expects($this->at(1))
->method('writeError')
->with($this->stringContains('Downgrading'));
$path = $this->getUniqueTmpDirectory();
touch($path.'/script.js');
touch($path.'_script.js');
$filesystem = $this->getMock('Composer\Util\Filesystem');
$filesystem->expects($this->once())
->method('removeDirectory')
->will($this->returnValue(true));
$downloader = $this->getDownloader($ioMock, null, null, null, null, $filesystem);
$promise = $downloader->download($newPackage, $path, $oldPackage);
$loop = new Loop($this->httpDownloader);
$loop->wait(array($promise));
$downloader->update($oldPackage, $newPackage, $path);
}
}

View File

@ -56,7 +56,7 @@ class FossilDownloaderTest extends TestCase
->will($this->returnValue(null));
$downloader = $this->getDownloaderMock();
$downloader->download($packageMock, '/path');
$downloader->install($packageMock, '/path');
}
public function testDownload()
@ -89,7 +89,7 @@ class FossilDownloaderTest extends TestCase
->will($this->returnValue(0));
$downloader = $this->getDownloaderMock(null, null, $processExecutor);
$downloader->download($packageMock, 'repo');
$downloader->install($packageMock, 'repo');
}
/**

View File

@ -79,7 +79,7 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue(null));
$downloader = $this->getDownloaderMock();
$downloader->download($packageMock, '/path');
$downloader->install($packageMock, '/path');
}
public function testDownload()
@ -130,7 +130,7 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue(0));
$downloader = $this->getDownloaderMock(null, null, $processExecutor);
$downloader->download($packageMock, 'composerPath');
$downloader->install($packageMock, 'composerPath');
}
public function testDownloadWithCache()
@ -195,7 +195,7 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue(0));
$downloader = $this->getDownloaderMock(null, $config, $processExecutor);
$downloader->download($packageMock, 'composerPath');
$downloader->install($packageMock, 'composerPath');
@rmdir($cachePath);
}
@ -265,7 +265,7 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue(0));
$downloader = $this->getDownloaderMock(null, new Config(), $processExecutor);
$downloader->download($packageMock, 'composerPath');
$downloader->install($packageMock, 'composerPath');
}
public function pushUrlProvider()
@ -329,7 +329,7 @@ class GitDownloaderTest extends TestCase
$config->merge(array('config' => array('github-protocols' => $protocols)));
$downloader = $this->getDownloaderMock(null, $config, $processExecutor);
$downloader->download($packageMock, 'composerPath');
$downloader->install($packageMock, 'composerPath');
}
/**
@ -360,7 +360,7 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue(1));
$downloader = $this->getDownloaderMock(null, null, $processExecutor);
$downloader->download($packageMock, 'composerPath');
$downloader->install($packageMock, 'composerPath');
}
/**

View File

@ -56,7 +56,7 @@ class HgDownloaderTest extends TestCase
->will($this->returnValue(null));
$downloader = $this->getDownloaderMock();
$downloader->download($packageMock, '/path');
$downloader->install($packageMock, '/path');
}
public function testDownload()
@ -83,7 +83,7 @@ class HgDownloaderTest extends TestCase
->will($this->returnValue(0));
$downloader = $this->getDownloaderMock(null, null, $processExecutor);
$downloader->download($packageMock, 'composerPath');
$downloader->install($packageMock, 'composerPath');
}
/**

View File

@ -17,6 +17,7 @@ use Composer\Config;
use Composer\Repository\VcsRepository;
use Composer\IO\IOInterface;
use Composer\Test\TestCase;
use Composer\Factory;
use Composer\Util\Filesystem;
/**
@ -96,7 +97,7 @@ class PerforceDownloaderTest extends TestCase
{
$repository = $this->getMockBuilder('Composer\Repository\VcsRepository')
->setMethods(array('getRepoConfig'))
->setConstructorArgs(array($repoConfig, $io, $config))
->setConstructorArgs(array($repoConfig, $io, $config, Factory::createHttpDownloader($io, $config)))
->getMock();
$repository->expects($this->any())->method('getRepoConfig')->will($this->returnValue($repoConfig));
@ -137,7 +138,7 @@ class PerforceDownloaderTest extends TestCase
$perforce->expects($this->at(5))->method('syncCodeBase')->with($label);
$perforce->expects($this->at(6))->method('cleanupClientSpec');
$this->downloader->setPerforce($perforce);
$this->downloader->doDownload($this->package, $this->testPath, 'url');
$this->downloader->doInstall($this->package, $this->testPath, 'url');
}
/**
@ -160,6 +161,6 @@ class PerforceDownloaderTest extends TestCase
$perforce->expects($this->at(5))->method('syncCodeBase')->with($label);
$perforce->expects($this->at(6))->method('cleanupClientSpec');
$this->downloader->setPerforce($perforce);
$this->downloader->doDownload($this->package, $this->testPath, 'url');
$this->downloader->doInstall($this->package, $this->testPath, 'url');
}
}

View File

@ -16,7 +16,8 @@ use Composer\Downloader\XzDownloader;
use Composer\Test\TestCase;
use Composer\Util\Filesystem;
use Composer\Util\Platform;
use Composer\Util\RemoteFilesystem;
use Composer\Util\Loop;
use Composer\Util\HttpDownloader;
class XzDownloaderTest extends TestCase
{
@ -66,10 +67,14 @@ class XzDownloaderTest extends TestCase
->method('get')
->with('vendor-dir')
->will($this->returnValue($this->testDir));
$downloader = new XzDownloader($io, $config, null, null, null, new RemoteFilesystem($io));
$downloader = new XzDownloader($io, $config, $httpDownloader = new HttpDownloader($io, $this->getMockBuilder('Composer\Config')->getMock()), null, null, null);
try {
$downloader->download($packageMock, $this->getUniqueTmpDirectory());
$promise = $downloader->download($packageMock, $this->testDir);
$loop = new Loop($httpDownloader);
$loop->wait(array($promise));
$downloader->install($packageMock, $this->testDir);
$this->fail('Download of invalid tarball should throw an exception');
} catch (\RuntimeException $e) {
$this->assertRegexp('/(File format not recognized|Unrecognized archive format)/i', $e->getMessage());

View File

@ -16,6 +16,8 @@ use Composer\Downloader\ZipDownloader;
use Composer\Package\PackageInterface;
use Composer\Test\TestCase;
use Composer\Util\Filesystem;
use Composer\Util\HttpDownloader;
use Composer\Util\Loop;
class ZipDownloaderTest extends TestCase
{
@ -26,12 +28,16 @@ class ZipDownloaderTest extends TestCase
private $prophet;
private $io;
private $config;
private $package;
public function setUp()
{
$this->testDir = $this->getUniqueTmpDirectory();
$this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
$this->config = $this->getMockBuilder('Composer\Config')->getMock();
$dlConfig = $this->getMockBuilder('Composer\Config')->getMock();
$this->httpDownloader = new HttpDownloader($this->io, $dlConfig);
$this->package = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock();
}
public function tearDown()
@ -64,42 +70,33 @@ class ZipDownloaderTest extends TestCase
}
$this->config->expects($this->at(0))
->method('get')
->with('disable-tls')
->will($this->returnValue(false));
$this->config->expects($this->at(1))
->method('get')
->with('cafile')
->will($this->returnValue(null));
$this->config->expects($this->at(2))
->method('get')
->with('capath')
->will($this->returnValue(null));
$this->config->expects($this->at(3))
->method('get')
->with('vendor-dir')
->will($this->returnValue($this->testDir));
$packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock();
$packageMock->expects($this->any())
$this->package->expects($this->any())
->method('getDistUrl')
->will($this->returnValue($distUrl = 'file://'.__FILE__))
;
$packageMock->expects($this->any())
$this->package->expects($this->any())
->method('getDistUrls')
->will($this->returnValue(array($distUrl)))
;
$packageMock->expects($this->atLeastOnce())
$this->package->expects($this->atLeastOnce())
->method('getTransportOptions')
->will($this->returnValue(array()))
;
$downloader = new ZipDownloader($this->io, $this->config);
$downloader = new ZipDownloader($this->io, $this->config, $this->httpDownloader);
$this->setPrivateProperty('hasSystemUnzip', false);
try {
$downloader->download($packageMock, sys_get_temp_dir().'/composer-zip-test');
$promise = $downloader->download($this->package, $path = sys_get_temp_dir().'/composer-zip-test');
$loop = new Loop($this->httpDownloader);
$loop->wait(array($promise));
$downloader->install($this->package, $path);
$this->fail('Download of invalid zip files should throw an exception');
} catch (\Exception $e) {
$this->assertContains('is not a zip archive', $e->getMessage());
@ -118,8 +115,7 @@ class ZipDownloaderTest extends TestCase
$this->setPrivateProperty('hasSystemUnzip', false);
$this->setPrivateProperty('hasZipArchive', true);
$downloader = new MockedZipDownloader($this->io, $this->config);
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader);
$zipArchive = $this->getMockBuilder('ZipArchive')->getMock();
$zipArchive->expects($this->at(0))
->method('open')
@ -129,7 +125,7 @@ class ZipDownloaderTest extends TestCase
->will($this->returnValue(false));
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
$downloader->extract('testfile.zip', 'vendor/dir');
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
}
/**
@ -144,8 +140,7 @@ class ZipDownloaderTest extends TestCase
$this->setPrivateProperty('hasSystemUnzip', false);
$this->setPrivateProperty('hasZipArchive', true);
$downloader = new MockedZipDownloader($this->io, $this->config);
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader);
$zipArchive = $this->getMockBuilder('ZipArchive')->getMock();
$zipArchive->expects($this->at(0))
->method('open')
@ -155,7 +150,7 @@ class ZipDownloaderTest extends TestCase
->will($this->throwException(new \ErrorException('Not a directory')));
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
$downloader->extract('testfile.zip', 'vendor/dir');
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
}
/**
@ -169,8 +164,7 @@ class ZipDownloaderTest extends TestCase
$this->setPrivateProperty('hasSystemUnzip', false);
$this->setPrivateProperty('hasZipArchive', true);
$downloader = new MockedZipDownloader($this->io, $this->config);
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader);
$zipArchive = $this->getMockBuilder('ZipArchive')->getMock();
$zipArchive->expects($this->at(0))
->method('open')
@ -180,7 +174,7 @@ class ZipDownloaderTest extends TestCase
->will($this->returnValue(true));
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
$downloader->extract('testfile.zip', 'vendor/dir');
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
}
/**
@ -200,8 +194,8 @@ class ZipDownloaderTest extends TestCase
->method('execute')
->will($this->returnValue(1));
$downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor);
$downloader->extract('testfile.zip', 'vendor/dir');
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor);
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
}
public function testSystemUnzipOnlyGood()
@ -217,8 +211,8 @@ class ZipDownloaderTest extends TestCase
->method('execute')
->will($this->returnValue(0));
$downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor);
$downloader->extract('testfile.zip', 'vendor/dir');
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor);
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
}
public function testNonWindowsFallbackGood()
@ -244,9 +238,9 @@ class ZipDownloaderTest extends TestCase
->method('extractTo')
->will($this->returnValue(true));
$downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor);
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor);
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
$downloader->extract('testfile.zip', 'vendor/dir');
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
}
/**
@ -276,9 +270,9 @@ class ZipDownloaderTest extends TestCase
->method('extractTo')
->will($this->returnValue(false));
$downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor);
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor);
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
$downloader->extract('testfile.zip', 'vendor/dir');
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
}
public function testWindowsFallbackGood()
@ -304,9 +298,9 @@ class ZipDownloaderTest extends TestCase
->method('extractTo')
->will($this->returnValue(false));
$downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor);
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor);
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
$downloader->extract('testfile.zip', 'vendor/dir');
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
}
/**
@ -336,9 +330,9 @@ class ZipDownloaderTest extends TestCase
->method('extractTo')
->will($this->returnValue(false));
$downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor);
$downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor);
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
$downloader->extract('testfile.zip', 'vendor/dir');
$downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
}
}
@ -349,8 +343,13 @@ class MockedZipDownloader extends ZipDownloader
return;
}
public function extract($file, $path)
public function install(PackageInterface $package, $path, $output = true)
{
parent::extract($file, $path);
return;
}
public function extract(PackageInterface $package, $file, $path)
{
parent::extract($package, $file, $path);
}
}

View File

@ -101,7 +101,7 @@ class EventDispatcherTest extends TestCase
$composer->setPackage($package);
$composer->setRepositoryManager($this->getRepositoryManagerMockForDevModePassingTest());
$composer->setInstallationManager($this->getMockBuilder('Composer\Installer\InstallationManager')->getMock());
$composer->setInstallationManager($this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock());
$dispatcher = new EventDispatcher(
$composer,

View File

@ -35,6 +35,6 @@ class FactoryTest extends TestCase
->with($this->equalTo('disable-tls'))
->will($this->returnValue(true));
Factory::createRemoteFilesystem($ioMock, $config);
Factory::createHttpDownloader($ioMock, $config);
}
}

View File

@ -13,6 +13,7 @@
namespace Composer\Test\Installer;
use Composer\Installer\InstallationManager;
use Composer\Installer\NoopInstaller;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Operation\UninstallOperation;
@ -21,9 +22,11 @@ use PHPUnit\Framework\TestCase;
class InstallationManagerTest extends TestCase
{
protected $repository;
protected $loop;
public function setUp()
{
$this->loop = $this->getMockBuilder('Composer\Util\Loop')->disableOriginalConstructor()->getMock();
$this->repository = $this->getMockBuilder('Composer\Repository\InstalledRepositoryInterface')->getMock();
}
@ -38,7 +41,7 @@ class InstallationManagerTest extends TestCase
return $arg === 'vendor';
}));
$manager = new InstallationManager();
$manager = new InstallationManager($this->loop);
$manager->addInstaller($installer);
$this->assertSame($installer, $manager->getInstaller('vendor'));
@ -67,7 +70,7 @@ class InstallationManagerTest extends TestCase
return $arg === 'vendor';
}));
$manager = new InstallationManager();
$manager = new InstallationManager($this->loop);
$manager->addInstaller($installer);
$this->assertSame($installer, $manager->getInstaller('vendor'));
@ -80,16 +83,21 @@ class InstallationManagerTest extends TestCase
public function testExecute()
{
$manager = $this->getMockBuilder('Composer\Installer\InstallationManager')
->setConstructorArgs(array($this->loop))
->setMethods(array('install', 'update', 'uninstall'))
->getMock();
$installOperation = new InstallOperation($this->createPackageMock());
$removeOperation = new UninstallOperation($this->createPackageMock());
$installOperation = new InstallOperation($package = $this->createPackageMock());
$removeOperation = new UninstallOperation($package);
$updateOperation = new UpdateOperation(
$this->createPackageMock(),
$this->createPackageMock()
$package,
$package
);
$package->expects($this->any())
->method('getType')
->will($this->returnValue('library'));
$manager
->expects($this->once())
->method('install')
@ -103,6 +111,7 @@ class InstallationManagerTest extends TestCase
->method('update')
->with($this->repository, $updateOperation);
$manager->addInstaller(new NoopInstaller());
$manager->execute($this->repository, $installOperation);
$manager->execute($this->repository, $removeOperation);
$manager->execute($this->repository, $updateOperation);
@ -111,7 +120,7 @@ class InstallationManagerTest extends TestCase
public function testInstall()
{
$installer = $this->createInstallerMock();
$manager = new InstallationManager();
$manager = new InstallationManager($this->loop);
$manager->addInstaller($installer);
$package = $this->createPackageMock();
@ -139,7 +148,7 @@ class InstallationManagerTest extends TestCase
public function testUpdateWithEqualTypes()
{
$installer = $this->createInstallerMock();
$manager = new InstallationManager();
$manager = new InstallationManager($this->loop);
$manager->addInstaller($installer);
$initial = $this->createPackageMock();
@ -173,18 +182,17 @@ class InstallationManagerTest extends TestCase
{
$libInstaller = $this->createInstallerMock();
$bundleInstaller = $this->createInstallerMock();
$manager = new InstallationManager();
$manager = new InstallationManager($this->loop);
$manager->addInstaller($libInstaller);
$manager->addInstaller($bundleInstaller);
$initial = $this->createPackageMock();
$target = $this->createPackageMock();
$operation = new UpdateOperation($initial, $target, 'test');
$initial
->expects($this->once())
->method('getType')
->will($this->returnValue('library'));
$target = $this->createPackageMock();
$target
->expects($this->once())
->method('getType')
@ -213,13 +221,14 @@ class InstallationManagerTest extends TestCase
->method('install')
->with($this->repository, $target);
$operation = new UpdateOperation($initial, $target, 'test');
$manager->update($this->repository, $operation);
}
public function testUninstall()
{
$installer = $this->createInstallerMock();
$manager = new InstallationManager();
$manager = new InstallationManager($this->loop);
$manager->addInstaller($installer);
$package = $this->createPackageMock();
@ -249,7 +258,7 @@ class InstallationManagerTest extends TestCase
$installer = $this->getMockBuilder('Composer\Installer\LibraryInstaller')
->disableOriginalConstructor()
->getMock();
$manager = new InstallationManager();
$manager = new InstallationManager($this->loop);
$manager->addInstaller($installer);
$package = $this->createPackageMock();
@ -281,7 +290,9 @@ class InstallationManagerTest extends TestCase
private function createPackageMock()
{
return $this->getMockBuilder('Composer\Package\PackageInterface')
$mock = $this->getMockBuilder('Composer\Package\PackageInterface')
->getMock();
return $mock;
}
}

View File

@ -113,7 +113,7 @@ class LibraryInstallerTest extends TestCase
$this->dm
->expects($this->once())
->method('download')
->method('install')
->with($package, $this->vendorDir.'/some/package');
$this->repository

View File

@ -63,7 +63,9 @@ class InstallerTest extends TestCase
->getMock();
$config = $this->getMockBuilder('Composer\Config')->getMock();
$repositoryManager = new RepositoryManager($io, $config);
$eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock();
$httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock();
$repositoryManager = new RepositoryManager($io, $config, $httpDownloader, $eventDispatcher);
$repositoryManager->setLocalRepository(new InstalledArrayRepository());
if (!is_array($repositories)) {
@ -76,7 +78,6 @@ class InstallerTest extends TestCase
$locker = $this->getMockBuilder('Composer\Package\Locker')->disableOriginalConstructor()->getMock();
$installationManager = new InstallationManagerMock();
$eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock();
$autoloadGenerator = $this->getMockBuilder('Composer\Autoload\AutoloadGenerator')->disableOriginalConstructor()->getMock();
$installer = new Installer($io, $config, clone $rootPackage, $downloadManager, $repositoryManager, $locker, $installationManager, $eventDispatcher, $autoloadGenerator);

View File

@ -20,6 +20,7 @@ use Composer\Repository\WritableRepositoryInterface;
use Composer\Installer;
use Composer\IO\IOInterface;
use Composer\Test\TestCase;
use Composer\Util\Loop;
class FactoryMock extends Factory
{
@ -39,9 +40,9 @@ class FactoryMock extends Factory
{
}
protected function createInstallationManager()
public function createInstallationManager(Loop $loop)
{
return new InstallationManagerMock;
return new InstallationManagerMock();
}
protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io)

View File

@ -12,13 +12,11 @@
namespace Composer\Test\Mock;
use Composer\Util\RemoteFilesystem;
use Composer\Util\HttpDownloader;
use Composer\Util\Http\Response;
use Composer\Downloader\TransportException;
/**
* Remote filesystem mock
*/
class RemoteFilesystemMock extends RemoteFilesystem
class HttpDownloaderMock extends HttpDownloader
{
protected $contentMap;
@ -30,10 +28,10 @@ class RemoteFilesystemMock extends RemoteFilesystem
$this->contentMap = $contentMap;
}
public function getContents($originUrl, $fileUrl, $progress = true, $options = array())
public function get($fileUrl, $options = array())
{
if (!empty($this->contentMap[$fileUrl])) {
return $this->contentMap[$fileUrl];
return new Response(array('url' => $fileUrl), 200, array(), $this->contentMap[$fileUrl]);
}
throw new TransportException('The "'.$fileUrl.'" file could not be downloaded (NOT FOUND)', 404);

View File

@ -17,6 +17,7 @@ use Composer\Repository\RepositoryInterface;
use Composer\Repository\InstalledRepositoryInterface;
use Composer\Package\PackageInterface;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation;
@ -29,6 +30,18 @@ class InstallationManagerMock extends InstallationManager
private $uninstalled = array();
private $trace = array();
public function __construct()
{
}
public function execute(RepositoryInterface $repo, OperationInterface $operation)
{
$method = $operation->getJobType();
// skipping download() step here for tests
$this->$method($repo, $operation);
}
public function getInstallPath(PackageInterface $package)
{
return '';

View File

@ -12,9 +12,12 @@
namespace Composer\Test\Package\Archiver;
use Composer\IO\NullIO;
use Composer\Factory;
use Composer\Package\Archiver\ArchiveManager;
use Composer\Package\PackageInterface;
use Composer\Util\Loop;
use Composer\Test\Mock\FactoryMock;
class ArchiveManagerTest extends ArchiverTest
{
@ -30,7 +33,13 @@ class ArchiveManagerTest extends ArchiverTest
parent::setUp();
$factory = new Factory();
$this->manager = $factory->createArchiveManager($factory->createConfig());
$dm = $factory->createDownloadManager(
$io = new NullIO,
$config = FactoryMock::createConfig(),
$httpDownloader = $factory->createHttpDownloader($io, $config)
);
$loop = new Loop($httpDownloader);
$this->manager = $factory->createArchiveManager($factory->createConfig(), $dm, $loop);
$this->targetDir = $this->testDir.'/composer_archiver_tests';
}

View File

@ -89,7 +89,7 @@ class PluginInstallerTest extends TestCase
->method('getLocalRepository')
->will($this->returnValue($this->repository));
$im = $this->getMockBuilder('Composer\Installer\InstallationManager')->getMock();
$im = $this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock();
$im->expects($this->any())
->method('getInstallPath')
->will($this->returnCallback(function ($package) {

View File

@ -18,7 +18,7 @@ use Composer\Repository\RepositoryInterface;
use Composer\Test\Mock\FactoryMock;
use Composer\Test\TestCase;
use Composer\Package\Loader\ArrayLoader;
use Composer\Semver\VersionParser;
use Composer\Package\Version\VersionParser;
class ComposerRepositoryTest extends TestCase
{
@ -32,11 +32,13 @@ class ComposerRepositoryTest extends TestCase
);
$repository = $this->getMockBuilder('Composer\Repository\ComposerRepository')
->setMethods(array('loadRootServerFile', 'createPackage'))
->setMethods(array('loadRootServerFile', 'createPackages'))
->setConstructorArgs(array(
$repoConfig,
new NullIO,
FactoryMock::createConfig(),
$this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(),
$this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock()
))
->getMock();
@ -45,16 +47,17 @@ class ComposerRepositoryTest extends TestCase
->method('loadRootServerFile')
->will($this->returnValue($repoPackages));
$stubs = array();
foreach ($expected as $at => $arg) {
$stubPackage = $this->getPackage('stub/stub', '1.0.0');
$repository
->expects($this->at($at + 2))
->method('createPackage')
->with($this->identicalTo($arg), $this->equalTo('Composer\Package\CompletePackage'))
->will($this->returnValue($stubPackage));
$stubs[] = $this->getPackage('stub/stub', '1.0.0');
}
$repository
->expects($this->at(2))
->method('createPackages')
->with($this->identicalTo($expected), $this->equalTo('Composer\Package\CompletePackage'))
->will($this->returnValue($stubs));
// Triggers initialization
$packages = $repository->getPackages();
@ -143,19 +146,12 @@ class ComposerRepositoryTest extends TestCase
)));
$versionParser = new VersionParser();
$repo->setRootAliases(array(
'a' => array(
$versionParser->normalize('0.6') => array('alias' => 'dev-feature', 'alias_normalized' => $versionParser->normalize('dev-feature')),
$versionParser->normalize('1.1.x-dev') => array('alias' => '1.0', 'alias_normalized' => $versionParser->normalize('1.0')),
),
));
$reflMethod = new \ReflectionMethod($repo, 'whatProvides');
$reflMethod->setAccessible(true);
$packages = $reflMethod->invoke($repo, 'a', array($this, 'isPackageAcceptableReturnTrue'));
$packages = $repo->whatProvides('a', false, array($this, 'isPackageAcceptableReturnTrue'));
$this->assertCount(7, $packages);
$this->assertEquals(array('1', '1-alias', '2', '2-alias', '2-root', '3', '3-root'), array_keys($packages));
$this->assertInstanceOf('Composer\Package\AliasPackage', $packages['2-root']);
$this->assertSame($packages['2'], $packages['2-root']->getAliasOf());
$this->assertCount(5, $packages);
$this->assertEquals(array('1', '1-alias', '2', '2-alias', '3'), array_keys($packages));
$this->assertSame($packages['2'], $packages['2-alias']->getAliasOf());
}
@ -179,21 +175,29 @@ class ComposerRepositoryTest extends TestCase
),
);
$rfs = $this->getMockBuilder('Composer\Util\RemoteFilesystem')
$httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader')
->disableOriginalConstructor()
->getMock();
$eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
->disableOriginalConstructor()
->getMock();
$rfs->expects($this->at(0))
->method('getContents')
->with('example.org', 'http://example.org/packages.json', false)
->willReturn(json_encode(array('search' => '/search.json?q=%query%&type=%type%')));
$httpDownloader->expects($this->at(0))
->method('get')
->with($url = 'http://example.org/packages.json')
->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array('search' => '/search.json?q=%query%&type=%type%'))));
$rfs->expects($this->at(1))
->method('getContents')
->with('example.org', 'http://example.org/search.json?q=foo&type=composer-plugin', false)
->willReturn(json_encode($result));
$httpDownloader->expects($this->at(1))
->method('get')
->with($url = 'http://example.org/search.json?q=foo&type=composer-plugin')
->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode($result)));
$repository = new ComposerRepository($repoConfig, new NullIO, FactoryMock::createConfig(), null, $rfs);
$httpDownloader->expects($this->at(2))
->method('get')
->with($url = 'http://example.org/search.json?q=foo&type=library')
->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array())));
$repository = new ComposerRepository($repoConfig, new NullIO, FactoryMock::createConfig(), $httpDownloader, $eventDispatcher);
$this->assertSame(
array(array('name' => 'foo', 'description' => null)),

View File

@ -14,8 +14,8 @@ namespace Composer\Test\Repository;
use Composer\Package\Loader\ArrayLoader;
use Composer\Repository\PathRepository;
use Composer\Semver\VersionParser;
use Composer\Test\TestCase;
use Composer\Package\Version\VersionParser;
class PathRepositoryTest extends TestCase
{

View File

@ -22,19 +22,19 @@ use Composer\Semver\VersionParser;
use Composer\Semver\Constraint\Constraint;
use Composer\Package\Link;
use Composer\Package\CompletePackage;
use Composer\Test\Mock\RemoteFilesystemMock;
use Composer\Test\Mock\HttpDownloaderMock;
class ChannelReaderTest extends TestCase
{
public function testShouldBuildPackagesFromPearSchema()
{
$rfs = new RemoteFilesystemMock(array(
$httpDownloader = new HttpDownloaderMock(array(
'http://pear.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.1.xml'),
'http://test.loc/rest11/c/categories.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/categories.xml'),
'http://test.loc/rest11/c/Default/packagesinfo.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/packagesinfo.xml'),
));
$reader = new \Composer\Repository\Pear\ChannelReader($rfs);
$reader = new \Composer\Repository\Pear\ChannelReader($httpDownloader);
$channelInfo = $reader->read('http://pear.net/');
$packages = $channelInfo->getPackages();
@ -50,17 +50,21 @@ class ChannelReaderTest extends TestCase
public function testShouldSelectCorrectReader()
{
$rfs = new RemoteFilesystemMock(array(
$httpDownloader = new HttpDownloaderMock(array(
'http://pear.1.0.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.0.xml'),
'http://test.loc/rest10/p/packages.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/packages.xml'),
'http://test.loc/rest10/p/http_client/info.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_info.xml'),
'http://test.loc/rest10/r/http_client/allreleases.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_allreleases.xml'),
'http://test.loc/rest10/r/http_client/deps.1.2.1.txt' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_client_deps.1.2.1.txt'),
'http://test.loc/rest10/p/http_request/info.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_request_info.xml'),
'http://test.loc/rest10/r/http_request/allreleases.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_request_allreleases.xml'),
'http://test.loc/rest10/r/http_request/deps.1.4.0.txt' => file_get_contents(__DIR__ . '/Fixtures/Rest1.0/http_request_deps.1.4.0.txt'),
'http://pear.1.1.net/channel.xml' => file_get_contents(__DIR__ . '/Fixtures/channel.1.1.xml'),
'http://test.loc/rest11/c/categories.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/categories.xml'),
'http://test.loc/rest11/c/Default/packagesinfo.xml' => file_get_contents(__DIR__ . '/Fixtures/Rest1.1/packagesinfo.xml'),
));
$reader = new \Composer\Repository\Pear\ChannelReader($rfs);
$reader = new \Composer\Repository\Pear\ChannelReader($httpDownloader);
$reader->read('http://pear.1.0.net/');
$reader->read('http://pear.1.1.net/');

Some files were not shown because too many files have changed in this diff Show More