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/console": "^2.7 || ^3.0 || ^4.0",
"symfony/filesystem": "^2.7 || ^3.0 || ^4.0", "symfony/filesystem": "^2.7 || ^3.0 || ^4.0",
"symfony/finder": "^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": { "conflict": {
"symfony/console": "2.8.38" "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", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "e46280c4cfd37bf3ec8be36095feb20e", "content-hash": "b078b12b2912d599e0c6904f64def484",
"packages": [ "packages": [
{ {
"name": "composer/ca-bundle", "name": "composer/ca-bundle",
@ -342,6 +342,50 @@
], ],
"time": "2018-11-20T15:27:04+00:00" "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", "name": "seld/jsonlint",
"version": "1.7.1", "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 Defaults to `1`. If set to `0`, Composer will not create `.htaccess` files in the
composer home, cache, and data directories. 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) → ← [Libraries](02-libraries.md) | [Schema](04-schema.md) →

View File

@ -176,8 +176,8 @@ class AwsPlugin implements PluginInterface, EventSubscriberInterface
if ($protocol === 's3') { if ($protocol === 's3') {
$awsClient = new AwsClient($this->io, $this->composer->getConfig()); $awsClient = new AwsClient($this->io, $this->composer->getConfig());
$s3RemoteFilesystem = new S3RemoteFilesystem($this->io, $event->getRemoteFilesystem()->getOptions(), $awsClient); $s3Downloader = new S3Downloader($this->io, $event->getHttpDownloader()->getOptions(), $awsClient);
$event->setRemoteFilesystem($s3RemoteFilesystem); $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 - **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. provides you with access to the input and output objects of the program.
- **pre-file-download**: occurs before files are downloaded and allows - **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. based on the URL to be downloaded.
- **pre-command-run**: occurs before a command is executed and allows you to - **pre-command-run**: occurs before a command is executed and allows you to
manipulate the `InputInterface` object's options and arguments to tweak 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\CommandEvent;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Loop;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
@ -104,8 +105,9 @@ EOT
$archiveManager = $composer->getArchiveManager(); $archiveManager = $composer->getArchiveManager();
} else { } else {
$factory = new Factory; $factory = new Factory;
$downloadManager = $factory->createDownloadManager($io, $config); $httpDownloader = $factory->createHttpDownloader($io, $config);
$archiveManager = $factory->createArchiveManager($config, $downloadManager); $downloadManager = $factory->createDownloadManager($io, $config, $httpDownloader);
$archiveManager = $factory->createArchiveManager($config, $downloadManager, new Loop($httpDownloader));
} }
if ($packageName) { if ($packageName) {

View File

@ -38,6 +38,7 @@ use Symfony\Component\Finder\Finder;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Config\JsonConfigSource; use Composer\Config\JsonConfigSource;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Loop;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
/** /**
@ -161,7 +162,6 @@ EOT
} }
$composer = Factory::create($io, null, $disablePlugins); $composer = Factory::create($io, null, $disablePlugins);
$composer->getDownloadManager()->setOutputProgress(!$noProgress);
$fs = new Filesystem(); $fs = new Filesystem();
@ -345,15 +345,17 @@ EOT
$package = $package->getAliasOf(); $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) $dm->setPreferSource($preferSource)
->setPreferDist($preferDist) ->setPreferDist($preferDist);
->setOutputProgress(!$noProgress);
$projectInstaller = new ProjectInstaller($directory, $dm); $projectInstaller = new ProjectInstaller($directory, $dm);
$im = $this->createInstallationManager(); $im = $factory->createInstallationManager(new Loop($httpDownloader));
$im->addInstaller($projectInstaller); $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); $im->notifyInstalls($io);
// collect suggestions // collect suggestions
@ -369,16 +371,4 @@ EOT
return $installedFromVcs; 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\ConfigValidator;
use Composer\Util\IniHelper; use Composer\Util\IniHelper;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
use Composer\Util\StreamContextFactory; use Composer\Util\StreamContextFactory;
use Composer\SelfUpdate\Keys; use Composer\SelfUpdate\Keys;
use Composer\SelfUpdate\Versions; use Composer\SelfUpdate\Versions;
@ -35,8 +35,8 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
class DiagnoseCommand extends BaseCommand class DiagnoseCommand extends BaseCommand
{ {
/** @var RemoteFilesystem */ /** @var HttpDownloader */
protected $rfs; protected $httpDownloader;
/** @var ProcessExecutor */ /** @var ProcessExecutor */
protected $process; protected $process;
@ -85,7 +85,7 @@ EOT
$config->merge(array('config' => array('secure-http' => false))); $config->merge(array('config' => array('secure-http' => false)));
$config->prohibitUrlByConfig('http://repo.packagist.org', new NullIO); $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); $this->process = new ProcessExecutor($io);
$io->write('Checking platform settings: ', false); $io->write('Checking platform settings: ', false);
@ -226,7 +226,7 @@ EOT
} }
try { 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) { } catch (TransportException $e) {
if (false !== strpos($e->getMessage(), 'cafile')) { if (false !== strpos($e->getMessage(), 'cafile')) {
$result[] = '<error>[' . get_class($e) . '] ' . $e->getMessage() . '</error>'; $result[] = '<error>[' . get_class($e) . '] ' . $e->getMessage() . '</error>';
@ -253,11 +253,11 @@ EOT
$protocol = extension_loaded('openssl') ? 'https' : 'http'; $protocol = extension_loaded('openssl') ? 'https' : 'http';
try { 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 = reset($json['provider-includes']);
$hash = $hash['sha256']; $hash = $hash['sha256'];
$path = str_replace('%hash%', $hash, key($json['provider-includes'])); $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) { if (hash('sha256', $provider) !== $hash) {
return 'It seems that your proxy is modifying http traffic on the fly'; 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'; $url = 'http://repo.packagist.org/packages.json';
try { try {
$this->rfs->getContents('packagist.org', $url, false); $this->httpDownloader->get($url);
} catch (TransportException $e) { } catch (TransportException $e) {
try { 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) { } catch (TransportException $e) {
return 'Unable to assess the situation, maybe packagist.org is down ('.$e->getMessage().')'; 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'; $url = 'https://api.github.com/repos/Seldaek/jsonlint/zipball/1.0.0';
try { try {
$this->rfs->getContents('github.com', $url, false); $this->httpDownloader->get($url);
} catch (TransportException $e) { } catch (TransportException $e) {
try { 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) { } catch (TransportException $e) {
return 'Unable to assess the situation, maybe github is down ('.$e->getMessage().')'; return 'Unable to assess the situation, maybe github is down ('.$e->getMessage().')';
} }
@ -344,7 +344,7 @@ EOT
try { try {
$url = $domain === 'github.com' ? 'https://api.'.$domain.'/' : 'https://'.$domain.'/api/v3/'; $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, 'retry-auth-failure' => false,
)) ? true : 'Unexpected error'; )) ? true : 'Unexpected error';
} catch (\Exception $e) { } catch (\Exception $e) {
@ -374,8 +374,7 @@ EOT
} }
$url = $domain === 'github.com' ? 'https://api.'.$domain.'/rate_limit' : 'https://'.$domain.'/api/rate_limit'; $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 = $this->httpDownloader->get($url, array('retry-auth-failure' => false))->parseJson();
$data = json_decode($json, true);
return $data['resources']['core']; return $data['resources']['core'];
} }
@ -428,7 +427,7 @@ EOT
return $result; return $result;
} }
$versionsUtil = new Versions($config, $this->rfs); $versionsUtil = new Versions($config, $this->httpDownloader);
$latest = $versionsUtil->getLatest(); $latest = $versionsUtil->getLatest();
if (Composer::VERSION !== $latest['version'] && Composer::VERSION !== '@package_version@') { 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 = $this->getComposer(true, $input->getOption('no-plugins'));
$composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'install', $input, $output); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'install', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);

View File

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

View File

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

View File

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

View File

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

View File

@ -89,7 +89,7 @@ EOT
// list packages // list packages
foreach ($installedRepo->getCanonicalPackages() as $package) { foreach ($installedRepo->getCanonicalPackages() as $package) {
$downloader = $dm->getDownloaderForInstalledPackage($package); $downloader = $dm->getDownloaderForPackage($package);
$targetDir = $im->getInstallPath($package); $targetDir = $im->getInstallPath($package);
if ($downloader instanceof ChangeReportInterface) { 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); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output);
$composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);

View File

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

View File

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

View File

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

View File

@ -30,33 +30,50 @@ abstract class ArchiveDownloader extends FileDownloader
* @throws \RuntimeException * @throws \RuntimeException
* @throws \UnexpectedValueException * @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); if ($output) {
$retries = 3; $this->io->writeError(" - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)");
while ($retries--) { }
$fileName = parent::download($package, $path, $output);
if ($output) { $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8);
$this->io->writeError(' Extracting archive', false, IOInterface::VERBOSE); $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->unlink($fileName);
$this->filesystem->ensureDirectoryExists($temporaryDir);
try { $renameAsOne = false;
$this->extract($fileName, $temporaryDir); if (!file_exists($path) || ($this->filesystem->isDirEmpty($path) && $this->filesystem->removeDirectory($path))) {
} catch (\Exception $e) { $renameAsOne = true;
// remove cache if the file was corrupted }
parent::clearLastCacheWrite($package);
throw $e; $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->rename($extractedDir, $path);
$this->filesystem->unlink($fileName); } else {
$contentDir = $this->getFolderContent($temporaryDir);
// only one dir in the archive, extract its contents out of it // 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)); $contentDir = $this->getFolderContent((string) reset($contentDir));
} }
@ -65,35 +82,24 @@ abstract class ArchiveDownloader extends FileDownloader
$file = (string) $file; $file = (string) $file;
$this->filesystem->rename($file, $path . '/' . basename($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) 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 * @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 * Returns the folder content, excluding dotfiles

View File

@ -15,6 +15,7 @@ namespace Composer\Downloader;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use React\Promise\PromiseInterface;
/** /**
* Downloaders manager. * Downloaders manager.
@ -24,6 +25,7 @@ use Composer\Util\Filesystem;
class DownloadManager class DownloadManager
{ {
private $io; private $io;
private $httpDownloader;
private $preferDist = false; private $preferDist = false;
private $preferSource = false; private $preferSource = false;
private $packagePreferences = array(); private $packagePreferences = array();
@ -33,9 +35,9 @@ class DownloadManager
/** /**
* Initializes download manager. * Initializes download manager.
* *
* @param IOInterface $io The Input Output Interface * @param IOInterface $io The Input Output Interface
* @param bool $preferSource prefer downloading from source * @param bool $preferSource prefer downloading from source
* @param Filesystem|null $filesystem custom Filesystem object * @param Filesystem|null $filesystem custom Filesystem object
*/ */
public function __construct(IOInterface $io, $preferSource = false, Filesystem $filesystem = null) public function __construct(IOInterface $io, $preferSource = false, Filesystem $filesystem = null)
{ {
@ -83,22 +85,6 @@ class DownloadManager
return $this; 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. * Sets installer downloader for a specific installation type.
* *
@ -140,7 +126,7 @@ class DownloadManager
* wrong type * wrong type
* @return DownloaderInterface|null * @return DownloaderInterface|null
*/ */
public function getDownloaderForInstalledPackage(PackageInterface $package) public function getDownloaderForPackage(PackageInterface $package)
{ {
$installationSource = $package->getInstallationSource(); $installationSource = $package->getInstallationSource();
@ -154,7 +140,7 @@ class DownloadManager
$downloader = $this->getDownloader($package->getSourceType()); $downloader = $this->getDownloader($package->getSourceType());
} else { } else {
throw new \InvalidArgumentException( 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; return $downloader;
} }
public function getDownloaderType(DownloaderInterface $downloader)
{
return array_search($downloader, $this->downloaders);
}
/** /**
* Downloads package into target dir. * Downloads package into target dir.
* *
* @param PackageInterface $package package instance * @param PackageInterface $package package instance
* @param string $targetDir target dir * @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 \InvalidArgumentException if package have no urls to download from
* @throws \RuntimeException * @throws \RuntimeException
*/ */
public function download(PackageInterface $package, $targetDir, $preferSource = null) public function install(PackageInterface $package, $targetDir)
{ {
$preferSource = null !== $preferSource ? $preferSource : $this->preferSource; $downloader = $this->getDownloaderForPackage($package);
$sourceType = $package->getSourceType(); if ($downloader) {
$distType = $package->getDistType(); $downloader->install($package, $targetDir);
$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>'
);
}
} }
} }
@ -242,31 +260,23 @@ class DownloadManager
*/ */
public function update(PackageInterface $initial, PackageInterface $target, $targetDir) 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) { if (!$downloader) {
$initialDownloader->remove($initial, $targetDir);
return; return;
} }
$installationSource = $initial->getInstallationSource(); $initialType = $this->getDownloaderType($initialDownloader);
$targetType = $this->getDownloaderType($downloader);
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;
}
if ($initialType === $targetType) { if ($initialType === $targetType) {
$target->setInstallationSource($installationSource);
try { try {
$downloader->update($initial, $target, $targetDir); $downloader->update($initial, $target, $targetDir);
@ -282,8 +292,12 @@ class DownloadManager
} }
} }
$downloader->remove($initial, $targetDir); // if downloader type changed, or update failed and user asks for reinstall,
$this->download($target, $targetDir, 'source' === $installationSource); // 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) public function remove(PackageInterface $package, $targetDir)
{ {
$downloader = $this->getDownloaderForInstalledPackage($package); $downloader = $this->getDownloaderForPackage($package);
if ($downloader) { if ($downloader) {
$downloader->remove($package, $targetDir); $downloader->remove($package, $targetDir);
} }
@ -322,4 +336,48 @@ class DownloadManager
return $package->isDev() ? 'source' : 'dist'; 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; namespace Composer\Downloader;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use React\Promise\PromiseInterface;
/** /**
* Downloader interface. * Downloader interface.
@ -29,13 +30,20 @@ interface DownloaderInterface
*/ */
public function getInstallationSource(); 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. * Downloads specific package into specific folder.
* *
* @param PackageInterface $package package instance * @param PackageInterface $package package instance
* @param string $path download path * @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. * Updates specific package in specific folder from initial to target version.
@ -53,12 +61,4 @@ interface DownloaderInterface
* @param string $path download path * @param string $path download path
*/ */
public function remove(PackageInterface $package, $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\Plugin\PreFileDownloadEvent;
use Composer\EventDispatcher\EventDispatcher; use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
use Composer\Util\Url as UrlUtil; use Composer\Util\Url as UrlUtil;
use Composer\Downloader\TransportException;
/** /**
* Base downloader for files * Base downloader for files
@ -39,11 +40,13 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
{ {
protected $io; protected $io;
protected $config; protected $config;
protected $rfs; protected $httpDownloader;
protected $filesystem; protected $filesystem;
protected $cache; 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; private $eventDispatcher;
/** /**
@ -51,17 +54,17 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
* *
* @param IOInterface $io The IO instance * @param IOInterface $io The IO instance
* @param Config $config The config * @param Config $config The config
* @param HttpDownloader $httpDownloader The remote filesystem
* @param EventDispatcher $eventDispatcher The event dispatcher * @param EventDispatcher $eventDispatcher The event dispatcher
* @param Cache $cache Optional cache instance * @param Cache $cache Cache instance
* @param RemoteFilesystem $rfs The remote filesystem
* @param Filesystem $filesystem The filesystem * @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->io = $io;
$this->config = $config; $this->config = $config;
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->rfs = $rfs ?: Factory::createRemoteFilesystem($this->io, $config); $this->httpDownloader = $httpDownloader;
$this->filesystem = $filesystem ?: new Filesystem(); $this->filesystem = $filesystem ?: new Filesystem();
$this->cache = $cache; $this->cache = $cache;
@ -87,121 +90,154 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
throw new \InvalidArgumentException('The given package is missing url information'); throw new \InvalidArgumentException('The given package is missing url information');
} }
if ($output) { $retries = 3;
$this->io->writeError(" - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): ", false);
}
$urls = $package->getDistUrls(); $urls = $package->getDistUrls();
while ($url = array_shift($urls)) { foreach ($urls as $index => $url) {
try { $processedUrl = $this->processUrl($package, $url);
$fileName = $this->doDownload($package, $path, $url); $urls[$index] = array(
break; 'base' => $url,
} catch (\Exception $e) { 'processed' => $processedUrl,
if ($this->io->isDebug()) { 'cacheKey' => $this->getCacheKey($package, $processedUrl)
$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;
}
}
} }
if ($output) {
$this->io->writeError('');
}
return $fileName;
}
protected function doDownload(PackageInterface $package, $path, $url)
{
$this->filesystem->emptyDirectory($path); $this->filesystem->emptyDirectory($path);
$fileName = $this->getFileName($package, $path); $fileName = $this->getFileName($package, $path);
$processedUrl = $this->processUrl($package, $url); $io = $this->io;
$hostname = parse_url($processedUrl, PHP_URL_HOST); $cache = $this->cache;
$httpDownloader = $this->httpDownloader;
$eventDispatcher = $this->eventDispatcher;
$filesystem = $this->filesystem;
$self = $this;
$preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->rfs, $processedUrl); $accept = null;
if ($this->eventDispatcher) { $reject = null;
$this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); $download = function () use ($io, $output, $httpDownloader, $cache, $eventDispatcher, $package, $fileName, $path, &$urls, &$accept, &$reject) {
} $url = reset($urls);
$rfs = $preFileDownloadEvent->getRemoteFilesystem();
if ($eventDispatcher) {
$preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed']);
$eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
}
try {
$checksum = $package->getDistSha1Checksum(); $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 // 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)) { if ($cache && (!$checksum || $checksum === $cache->sha1($cacheKey)) && $cache->copyTo($cacheKey, $fileName)) {
$this->io->writeError('Loading from cache', false); if ($output) {
$io->writeError(" - Loading <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>) from cache");
}
$result = \React\Promise\resolve($fileName);
} else { } else {
// download if cache restore failed if ($output) {
if (!$this->outputProgress) { $io->writeError(" - Downloading <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)");
$this->io->writeError('Downloading', false);
} }
// try to download 3 times then fail hard $result = $httpDownloader->addCopy($url['processed'], $fileName, $package->getTransportOptions())
$retries = 3; ->then($accept, $reject);
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);
}
} }
if (!file_exists($fileName)) { return $result->then(function ($result) use ($fileName, $checksum, $url) {
throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the' // in case of retry, the first call's Promise chain finally calls this twice at the end,
.' directory is writable and you have internet connectivity'); // 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) { $response->collect();
throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url.')');
} return $fileName;
} catch (\Exception $e) { };
$reject = function ($e) use ($io, &$urls, $download, $fileName, $path, $package, &$retries, $filesystem, $self) {
// clean up // clean up
$this->filesystem->removeDirectory($path); $filesystem->removeDirectory($path);
$this->clearLastCacheWrite($package); $self->clearLastCacheWrite($package);
throw $e;
}
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} * {@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()])) { if ($this->cache && isset($this->lastCacheWrites[$package->getName()])) {
$this->cache->remove($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->io->writeError(" - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>): ", false);
$this->remove($initial, $path, false); $this->remove($initial, $path, false);
$this->download($target, $path, false); $this->install($target, $path, false);
$this->io->writeError(''); $this->io->writeError('');
} }
@ -249,7 +285,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
*/ */
protected function getFileName(PackageInterface $package, $path) 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) public function getLocalChanges(PackageInterface $package, $targetDir)
{ {
$prevIO = $this->io; $prevIO = $this->io;
$prevProgress = $this->outputProgress;
$this->io = new NullIO; $this->io = new NullIO;
$this->io->loadConfiguration($this->config); $this->io->loadConfiguration($this->config);
$this->outputProgress = false;
$e = null; $e = null;
try { 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 = new Comparer();
$comparer->setSource($targetDir.'_compare'); $comparer->setSource($targetDir.'_compare');
@ -311,7 +347,6 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
} }
$this->io = $prevIO; $this->io = $prevIO;
$this->outputProgress = $prevProgress;
if ($e) { if ($e) {
throw $e; throw $e;

View File

@ -23,7 +23,7 @@ class FossilDownloader extends VcsDownloader
/** /**
* {@inheritDoc} * {@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 // Ensure we are allowed to use this URL by config
$this->config->prohibitUrlByConfig($url, $this->io); $this->config->prohibitUrlByConfig($url, $this->io);

View File

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

View File

@ -18,7 +18,7 @@ use Composer\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Util\Platform; use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
/** /**
@ -30,15 +30,16 @@ class GzipDownloader extends ArchiveDownloader
{ {
protected $process; 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); $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 // Try to use gunzip on *nix
if (!Platform::isWindows()) { if (!Platform::isWindows()) {
@ -63,14 +64,6 @@ class GzipDownloader extends ArchiveDownloader
$this->extractUsingExt($file, $targetFilepath); $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) private function extractUsingExt($file, $targetFilepath)
{ {
$archiveFile = gzopen($file, 'rb'); $archiveFile = gzopen($file, 'rb');

View File

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

View File

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

View File

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

View File

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

View File

@ -18,8 +18,9 @@ use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\IniHelper; use Composer\Util\IniHelper;
use Composer\Util\Platform; use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Package\PackageInterface;
use RarArchive; use RarArchive;
/** /**
@ -33,13 +34,13 @@ class RarDownloader extends ArchiveDownloader
{ {
protected $process; 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); $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; $processError = null;

View File

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

View File

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

View File

@ -55,6 +55,14 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
* {@inheritDoc} * {@inheritDoc}
*/ */
public function download(PackageInterface $package, $path) 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()) { if (!$package->getSourceReference()) {
throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information'); throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information');
@ -87,7 +95,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
$url = $needle . $url; $url = $needle . $url;
} }
} }
$this->doDownload($package, $path, $url); $this->doInstall($package, $path, $url);
break; break;
} catch (\Exception $e) { } catch (\Exception $e) {
// rethrow phpunit exceptions to avoid hard to debug bug failures // 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} * {@inheritDoc}
*/ */
@ -260,7 +259,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
* @param string $path download path * @param string $path download path
* @param string $url package url * @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. * 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\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
/** /**
@ -30,14 +30,14 @@ class XzDownloader extends ArchiveDownloader
{ {
protected $process; 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); $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); $command = 'tar -xJf ' . ProcessExecutor::escape($file) . ' -C ' . ProcessExecutor::escape($path);
@ -49,12 +49,4 @@ class XzDownloader extends ArchiveDownloader
throw new \RuntimeException($processError); 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\IniHelper;
use Composer\Util\Platform; use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\ExecutableFinder;
use ZipArchive; use ZipArchive;
@ -36,10 +36,10 @@ class ZipDownloader extends ArchiveDownloader
protected $process; protected $process;
private $zipArchiveObject; 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); $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 $file File to extract
* @param string $path Path where to extract file * @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 // Each extract calls its alternative if not available or fails
if (self::$isWindows) { if (self::$isWindows) {

View File

@ -23,7 +23,8 @@ use Composer\Repository\WritableRepositoryInterface;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Platform; use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
use Composer\Util\Loop;
use Composer\Util\Silencer; use Composer\Util\Silencer;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
use Composer\EventDispatcher\Event; use Composer\EventDispatcher\Event;
@ -325,14 +326,15 @@ class Factory
$io->loadConfiguration($config); $io->loadConfiguration($config);
} }
$rfs = self::createRemoteFilesystem($io, $config); $httpDownloader = self::createHttpDownloader($io, $config);
$loop = new Loop($httpDownloader);
// initialize event dispatcher // initialize event dispatcher
$dispatcher = new EventDispatcher($composer, $io); $dispatcher = new EventDispatcher($composer, $io);
$composer->setEventDispatcher($dispatcher); $composer->setEventDispatcher($dispatcher);
// initialize repository manager // initialize repository manager
$rm = RepositoryFactory::manager($io, $config, $dispatcher, $rfs); $rm = RepositoryFactory::manager($io, $config, $httpDownloader, $dispatcher);
$composer->setRepositoryManager($rm); $composer->setRepositoryManager($rm);
// load local repository // load local repository
@ -352,12 +354,12 @@ class Factory
$composer->setPackage($package); $composer->setPackage($package);
// initialize installation manager // initialize installation manager
$im = $this->createInstallationManager(); $im = $this->createInstallationManager($loop);
$composer->setInstallationManager($im); $composer->setInstallationManager($im);
if ($fullLoad) { if ($fullLoad) {
// initialize download manager // initialize download manager
$dm = $this->createDownloadManager($io, $config, $dispatcher, $rfs); $dm = $this->createDownloadManager($io, $config, $httpDownloader, $dispatcher);
$composer->setDownloadManager($dm); $composer->setDownloadManager($dm);
// initialize autoload generator // initialize autoload generator
@ -365,7 +367,7 @@ class Factory
$composer->setAutoloadGenerator($generator); $composer->setAutoloadGenerator($generator);
// initialize archive manager // initialize archive manager
$am = $this->createArchiveManager($config, $dm); $am = $this->createArchiveManager($config, $dm, $loop);
$composer->setArchiveManager($am); $composer->setArchiveManager($am);
} }
@ -451,7 +453,7 @@ class Factory
* @param EventDispatcher $eventDispatcher * @param EventDispatcher $eventDispatcher
* @return Downloader\DownloadManager * @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; $cache = null;
if ($config->get('cache-files-ttl') > 0) { 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('fossil', new Downloader\FossilDownloader($io, $config, $executor, $fs));
$dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $executor, $fs)); $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $executor, $fs));
$dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config)); $dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config));
$dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs)); $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
$dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs)); $dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
$dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $eventDispatcher, $cache, $rfs)); $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
$dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs)); $dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
$dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs)); $dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor));
$dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $eventDispatcher, $cache, $rfs)); $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
$dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $eventDispatcher, $cache, $rfs)); $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
$dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $eventDispatcher, $cache, $rfs)); $dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache));
return $dm; return $dm;
} }
@ -501,15 +503,9 @@ class Factory
* @param Downloader\DownloadManager $dm Manager use to download sources * @param Downloader\DownloadManager $dm Manager use to download sources
* @return Archiver\ArchiveManager * @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) { $am = new Archiver\ArchiveManager($dm, $loop);
$io = new IO\NullIO();
$io->loadConfiguration($config);
$dm = $this->createDownloadManager($io, $config);
}
$am = new Archiver\ArchiveManager($dm);
$am->addArchiver(new Archiver\ZipArchiver); $am->addArchiver(new Archiver\ZipArchiver);
$am->addArchiver(new Archiver\PharArchiver); $am->addArchiver(new Archiver\PharArchiver);
@ -531,9 +527,9 @@ class Factory
/** /**
* @return Installer\InstallationManager * @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 IOInterface $io IO instance
* @param Config $config Config instance * @param Config $config Config instance
* @param array $options Array of options passed directly to RemoteFilesystem constructor * @param array $options Array of options passed directly to HttpDownloader constructor
* @return RemoteFilesystem * @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; static $warned = false;
$disableTls = 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. ' 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.'); . '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 ($disableTls === false) {
if ($config && $config->get('cafile')) { if ($config && $config->get('cafile')) {
$remoteFilesystemOptions['ssl']['cafile'] = $config->get('cafile'); $httpDownloaderOptions['ssl']['cafile'] = $config->get('cafile');
} }
if ($config && $config->get('capath')) { 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 { try {
$remoteFilesystem = new RemoteFilesystem($io, $config, $remoteFilesystemOptions, $disableTls); $httpDownloader = new HttpDownloader($io, $config, $httpDownloaderOptions, $disableTls);
} catch (TransportException $e) { } catch (TransportException $e) {
if (false !== strpos($e->getMessage(), 'cafile')) { 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>'); $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; 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\MarkAliasInstalledOperation;
use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation;
use Composer\Util\StreamContextFactory; use Composer\Util\StreamContextFactory;
use Composer\Util\Loop;
/** /**
* Package operation manager. * Package operation manager.
@ -37,6 +38,12 @@ class InstallationManager
private $installers = array(); private $installers = array();
private $cache = array(); private $cache = array();
private $notifiablePackages = array(); private $notifiablePackages = array();
private $loop;
public function __construct(Loop $loop)
{
$this->loop = $loop;
}
public function reset() public function reset()
{ {
@ -156,7 +163,24 @@ class InstallationManager
*/ */
public function execute(RepositoryInterface $repo, OperationInterface $operation) public function execute(RepositoryInterface $repo, OperationInterface $operation)
{ {
// TODO this should take all operations in one go
$method = $operation->getJobType(); $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); $this->$method($repo, $operation);
} }
@ -194,7 +218,8 @@ class InstallationManager
$this->markForNotification($target); $this->markForNotification($target);
} else { } else {
$this->getInstaller($initialType)->uninstall($repo, $initial); $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\Package\PackageInterface;
use Composer\Repository\InstalledRepositoryInterface; use Composer\Repository\InstalledRepositoryInterface;
use InvalidArgumentException; use InvalidArgumentException;
use React\Promise\PromiseInterface;
/** /**
* Interface for the package installation manager. * Interface for the package installation manager.
@ -42,6 +43,15 @@ interface InstallerInterface
*/ */
public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package); 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. * Installs specific package.
* *

View File

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

View File

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

View File

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

View File

@ -50,13 +50,21 @@ class PluginInstaller extends LibraryInstaller
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function install(InstalledRepositoryInterface $repo, PackageInterface $package) public function download(PackageInterface $package, PackageInterface $prevPackage = null)
{ {
$extra = $package->getExtra(); $extra = $package->getExtra();
if (empty($extra['class'])) { 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.'); 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); parent::install($repo, $package);
try { try {
$this->composer->getPluginManager()->registerPackage($package, true); $this->composer->getPluginManager()->registerPackage($package, true);

View File

@ -58,7 +58,7 @@ class ProjectInstaller implements InstallerInterface
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function install(InstalledRepositoryInterface $repo, PackageInterface $package) public function download(PackageInterface $package, PackageInterface $prevPackage = null)
{ {
$installPath = $this->installPath; $installPath = $this->installPath;
if (file_exists($installPath) && !$this->filesystem->isDirEmpty($installPath)) { if (file_exists($installPath) && !$this->filesystem->isDirEmpty($installPath)) {
@ -67,7 +67,16 @@ class ProjectInstaller implements InstallerInterface
if (!is_dir($installPath)) { if (!is_dir($installPath)) {
mkdir($installPath, 0777, true); 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 JsonSchema\Validator;
use Seld\JsonLint\JsonParser; use Seld\JsonLint\JsonParser;
use Seld\JsonLint\ParsingException; use Seld\JsonLint\ParsingException;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Downloader\TransportException; use Composer\Downloader\TransportException;
@ -35,25 +35,25 @@ class JsonFile
const JSON_UNESCAPED_UNICODE = 256; const JSON_UNESCAPED_UNICODE = 256;
private $path; private $path;
private $rfs; private $httpDownloader;
private $io; private $io;
/** /**
* Initializes json file reader/parser. * Initializes json file reader/parser.
* *
* @param string $path path to a lockfile * @param string $path path to a lockfile
* @param RemoteFilesystem $rfs required for loading http/https json files * @param HttpDownloader $httpDownloader required for loading http/https json files
* @param IOInterface $io * @param IOInterface $io
* @throws \InvalidArgumentException * @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; $this->path = $path;
if (null === $rfs && preg_match('{^https?://}i', $path)) { if (null === $httpDownloader && preg_match('{^https?://}i', $path)) {
throw new \InvalidArgumentException('http urls require a RemoteFilesystem instance to be passed'); throw new \InvalidArgumentException('http urls require a HttpDownloader instance to be passed');
} }
$this->rfs = $rfs; $this->httpDownloader = $httpDownloader;
$this->io = $io; $this->io = $io;
} }
@ -84,8 +84,8 @@ class JsonFile
public function read() public function read()
{ {
try { try {
if ($this->rfs) { if ($this->httpDownloader) {
$json = $this->rfs->getContents($this->path, $this->path, false); $json = $this->httpDownloader->get($this->path)->getBody();
} else { } else {
if ($this->io && $this->io->isDebug()) { if ($this->io && $this->io->isDebug()) {
$this->io->writeError('Reading ' . $this->path); $this->io->writeError('Reading ' . $this->path);

View File

@ -16,6 +16,7 @@ use Composer\Downloader\DownloadManager;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Package\RootPackageInterface; use Composer\Package\RootPackageInterface;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Loop;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
/** /**
@ -25,6 +26,7 @@ use Composer\Json\JsonFile;
class ArchiveManager class ArchiveManager
{ {
protected $downloadManager; protected $downloadManager;
protected $loop;
protected $archivers = array(); protected $archivers = array();
@ -36,9 +38,10 @@ class ArchiveManager
/** /**
* @param DownloadManager $downloadManager A manager used to download package sources * @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->downloadManager = $downloadManager;
$this->loop = $loop;
} }
/** /**
@ -148,7 +151,9 @@ class ArchiveManager
$filesystem->ensureDirectoryExists($sourcePath); $filesystem->ensureDirectoryExists($sourcePath);
// Download sources // 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 // Check exclude from downloaded composer.json
if (file_exists($composerJsonPath = $sourcePath.'/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\RootAliasPackage;
use Composer\Package\RootPackageInterface; use Composer\Package\RootPackageInterface;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
use Composer\Semver\VersionParser as SemverVersionParser;
/** /**
* @author Konstantin Kudryashiv <ever.zet@gmail.com> * @author Konstantin Kudryashiv <ever.zet@gmail.com>
@ -29,7 +28,7 @@ class ArrayLoader implements LoaderInterface
protected $versionParser; protected $versionParser;
protected $loadOptions; protected $loadOptions;
public function __construct(SemverVersionParser $parser = null, $loadOptions = false) public function __construct(VersionParser $parser = null, $loadOptions = false)
{ {
if (!$parser) { if (!$parser) {
$parser = new VersionParser; $parser = new VersionParser;
@ -39,6 +38,69 @@ class ArrayLoader implements LoaderInterface
} }
public function load(array $config, $class = 'Composer\Package\CompletePackage') 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'])) { if (!isset($config['name'])) {
throw new \UnexpectedValueException('Unknown package has no name defined ('.json_encode($config).').'); throw new \UnexpectedValueException('Unknown package has no name defined ('.json_encode($config).').');
@ -53,7 +115,12 @@ class ArrayLoader implements LoaderInterface
} else { } else {
$version = $this->versionParser->normalize($config['version']); $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'); $package->setType(isset($config['type']) ? strtolower($config['type']) : 'library');
if (isset($config['target-dir'])) { 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'])) { if (isset($config['suggest']) && is_array($config['suggest'])) {
foreach ($config['suggest'] as $target => $reason) { foreach ($config['suggest'] as $target => $reason) {
if ('self.version' === trim($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'])) { if ($this->loadOptions && isset($config['transport-options'])) {
$package->setTransportOptions($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; 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 $source source package name
* @param string $sourceVersion source package version (pretty version ideally) * @param string $sourceVersion source package version (pretty version ideally)
@ -229,21 +311,26 @@ class ArrayLoader implements LoaderInterface
{ {
$res = array(); $res = array();
foreach ($links as $target => $constraint) { foreach ($links as $target => $constraint) {
if (!is_string($constraint)) { $res[strtolower($target)] = $this->createLink($source, $sourceVersion, $description, $target, $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);
} }
return $res; 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 * 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) // 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()); $branches = array_keys($driver->getBranches());
// try to find the best (nearest) version branch to assume this feature's version // try to find the best (nearest) version branch to assume this feature's version

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@
namespace Composer\Repository\Pear; namespace Composer\Repository\Pear;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
/** /**
* Base PEAR Channel reader. * Base PEAR Channel reader.
@ -33,12 +33,12 @@ abstract class BaseChannelReader
const ALL_RELEASES_NS = 'http://pear.php.net/dtd/rest.allreleases'; const ALL_RELEASES_NS = 'http://pear.php.net/dtd/rest.allreleases';
const PACKAGE_INFO_NS = 'http://pear.php.net/dtd/rest.package'; const PACKAGE_INFO_NS = 'http://pear.php.net/dtd/rest.package';
/** @var RemoteFilesystem */ /** @var HttpDownloader */
private $rfs; 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) protected function requestContent($origin, $path)
{ {
$url = rtrim($origin, '/') . '/' . ltrim($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) { if (!$content) {
throw new \UnexpectedValueException('The PEAR channel at ' . $url . ' did not respond.'); throw new \UnexpectedValueException('The PEAR channel at ' . $url . ' did not respond.');
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ use Composer\IO\IOInterface;
use Composer\Config; use Composer\Config;
use Composer\EventDispatcher\EventDispatcher; use Composer\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
/** /**
* Repositories manager. * Repositories manager.
@ -33,14 +33,14 @@ class RepositoryManager
private $io; private $io;
private $config; private $config;
private $eventDispatcher; 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->io = $io;
$this->config = $config; $this->config = $config;
$this->httpDownloader = $httpDownloader;
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->rfs = $rfs;
} }
/** /**
@ -127,8 +127,8 @@ class RepositoryManager
$reflMethod = new \ReflectionMethod($class, '__construct'); $reflMethod = new \ReflectionMethod($class, '__construct');
$params = $reflMethod->getParameters(); $params = $reflMethod->getParameters();
if (isset($params[4]) && $params[4]->getClass() && $params[4]->getClass()->getName() === 'Composer\Util\RemoteFilesystem') { if (isset($params[3]) && $params[3]->getClass() && $params[3]->getClass()->getName() === 'Composer\Util\HttpDownloader') {
return new $class($config, $this->io, $this->config, $this->eventDispatcher, $this->rfs); return new $class($config, $this->io, $this->config, $this->httpDownloader, $this->eventDispatcher);
} }
return new $class($config, $this->io, $this->config, $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\Downloader\TransportException;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Util\Bitbucket; use Composer\Util\Bitbucket;
use Composer\Util\Http\Response;
abstract class BitbucketDriver extends VcsDriver 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) { if ($this->fallbackDriver) {
return false; return false;
} }
@ -204,7 +205,7 @@ abstract class BitbucketDriver extends VcsDriver
$file $file
); );
return $this->getContentsWithOAuthCredentials($resource); return $this->fetchWithOAuthCredentials($resource)->getBody();
} }
/** /**
@ -222,7 +223,7 @@ abstract class BitbucketDriver extends VcsDriver
$this->repository, $this->repository,
$identifier $identifier
); );
$commit = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); $commit = $this->fetchWithOAuthCredentials($resource)->decodeJson();
return new \DateTime($commit['date']); return new \DateTime($commit['date']);
} }
@ -284,7 +285,7 @@ abstract class BitbucketDriver extends VcsDriver
); );
$hasNext = true; $hasNext = true;
while ($hasNext) { while ($hasNext) {
$tagsData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); $tagsData = $this->fetchWithOAuthCredentials($resource)->decodeJson();
foreach ($tagsData['values'] as $data) { foreach ($tagsData['values'] as $data) {
$this->tags[$data['name']] = $data['target']['hash']; $this->tags[$data['name']] = $data['target']['hash'];
} }
@ -328,7 +329,7 @@ abstract class BitbucketDriver extends VcsDriver
); );
$hasNext = true; $hasNext = true;
while ($hasNext) { while ($hasNext) {
$branchData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); $branchData = $this->fetchWithOAuthCredentials($resource)->decodeJson();
foreach ($branchData['values'] as $data) { foreach ($branchData['values'] as $data) {
// skip headless branches which seem to be deleted branches that bitbucket nevertheless returns in the API // skip headless branches which seem to be deleted branches that bitbucket nevertheless returns in the API
if ($this->vcsType === 'hg' && empty($data['heads'])) { if ($this->vcsType === 'hg' && empty($data['heads'])) {
@ -354,14 +355,14 @@ abstract class BitbucketDriver extends VcsDriver
* @param string $url The URL of content * @param string $url The URL of content
* @param bool $fetchingRepoData * @param bool $fetchingRepoData
* *
* @return mixed The result * @return Response The result
*/ */
protected function getContentsWithOAuthCredentials($url, $fetchingRepoData = false) protected function fetchWithOAuthCredentials($url, $fetchingRepoData = false)
{ {
try { try {
return parent::getContents($url); return parent::getContents($url);
} catch (TransportException $e) { } 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 (403 === $e->getCode() || (401 === $e->getCode() && strpos($e->getMessage(), 'Could not authenticate against') === 0)) {
if (!$this->io->hasAuthentication($this->originUrl) if (!$this->io->hasAuthentication($this->originUrl)
@ -371,7 +372,9 @@ abstract class BitbucketDriver extends VcsDriver
} }
if (!$this->io->isInteractive() && $fetchingRepoData) { 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 { try {
$this->setupFallbackDriver($this->generateSshUrl()); $this->setupFallbackDriver($this->generateSshUrl());
return true;
} catch (\RuntimeException $e) { } catch (\RuntimeException $e) {
$this->fallbackDriver = null; $this->fallbackDriver = null;
@ -433,7 +438,7 @@ abstract class BitbucketDriver extends VcsDriver
$this->repository $this->repository
); );
$data = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); $data = $this->fetchWithOAuthCredentials($resource)->decodeJson();
if (isset($data['mainbranch'])) { if (isset($data['mainbranch'])) {
return $data['mainbranch']; return $data['mainbranch'];
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -19,8 +19,9 @@ use Composer\Factory;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Http\Response;
/** /**
* A driver implementation for driver with authentication interaction. * A driver implementation for driver with authentication interaction.
@ -41,8 +42,8 @@ abstract class VcsDriver implements VcsDriverInterface
protected $config; protected $config;
/** @var ProcessExecutor */ /** @var ProcessExecutor */
protected $process; protected $process;
/** @var RemoteFilesystem */ /** @var HttpDownloader */
protected $remoteFilesystem; protected $httpDownloader;
/** @var array */ /** @var array */
protected $infoCache = array(); protected $infoCache = array();
/** @var Cache */ /** @var Cache */
@ -54,10 +55,10 @@ abstract class VcsDriver implements VcsDriverInterface
* @param array $repoConfig The repository configuration * @param array $repoConfig The repository configuration
* @param IOInterface $io The IO instance * @param IOInterface $io The IO instance
* @param Config $config The composer configuration * @param Config $config The composer configuration
* @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking
* @param ProcessExecutor $process Process instance, 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'])) { if (Filesystem::isLocalPath($repoConfig['url'])) {
$repoConfig['url'] = Filesystem::getPlatformPath($repoConfig['url']); $repoConfig['url'] = Filesystem::getPlatformPath($repoConfig['url']);
@ -68,8 +69,8 @@ abstract class VcsDriver implements VcsDriverInterface
$this->repoConfig = $repoConfig; $this->repoConfig = $repoConfig;
$this->io = $io; $this->io = $io;
$this->config = $config; $this->config = $config;
$this->process = $process ?: new ProcessExecutor($io); $this->httpDownloader = $httpDownloader;
$this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config); $this->process = $process;
} }
/** /**
@ -156,13 +157,13 @@ abstract class VcsDriver implements VcsDriverInterface
* *
* @param string $url The URL of content * @param string $url The URL of content
* *
* @return mixed The result * @return Response
*/ */
protected function getContents($url) protected function getContents($url)
{ {
$options = isset($this->repoConfig['options']) ? $this->repoConfig['options'] : array(); $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\InvalidPackageException;
use Composer\Package\Loader\LoaderInterface; use Composer\Package\Loader\LoaderInterface;
use Composer\EventDispatcher\EventDispatcher; use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\ProcessExecutor;
use Composer\Util\HttpDownloader;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Config; use Composer\Config;
@ -37,6 +39,8 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
protected $type; protected $type;
protected $loader; protected $loader;
protected $repoConfig; protected $repoConfig;
protected $httpDownloader;
protected $processExecutor;
protected $branchErrorOccurred = false; protected $branchErrorOccurred = false;
private $drivers; private $drivers;
/** @var VcsDriverInterface */ /** @var VcsDriverInterface */
@ -44,7 +48,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
/** @var VersionCacheInterface */ /** @var VersionCacheInterface */
private $versionCache; 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(); parent::__construct();
$this->drivers = $drivers ?: array( $this->drivers = $drivers ?: array(
@ -67,6 +71,8 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
$this->config = $config; $this->config = $config;
$this->repoConfig = $repoConfig; $this->repoConfig = $repoConfig;
$this->versionCache = $versionCache; $this->versionCache = $versionCache;
$this->httpDownloader = $httpDownloader;
$this->processExecutor = new ProcessExecutor($io);
} }
public function getRepoConfig() public function getRepoConfig()
@ -87,7 +93,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
if (isset($this->drivers[$this->type])) { if (isset($this->drivers[$this->type])) {
$class = $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(); $this->driver->initialize();
return $this->driver; return $this->driver;
@ -95,7 +101,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
foreach ($this->drivers as $driver) { foreach ($this->drivers as $driver) {
if ($driver::supports($this->io, $this->config, $this->url)) { 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(); $this->driver->initialize();
return $this->driver; return $this->driver;
@ -104,7 +110,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
foreach ($this->drivers as $driver) { foreach ($this->drivers as $driver) {
if ($driver::supports($this->io, $this->config, $this->url, true)) { 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(); $this->driver->initialize();
return $this->driver; return $this->driver;

View File

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

View File

@ -14,6 +14,7 @@ namespace Composer\Util;
use Composer\Config; use Composer\Config;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Downloader\TransportException;
/** /**
* @author Jordi Boggiano <j.boggiano@seld.be> * @author Jordi Boggiano <j.boggiano@seld.be>
@ -29,7 +30,11 @@ class AuthHelper
$this->config = $config; $this->config = $config;
} }
public function storeAuth($originUrl, $storeAuth) /**
* @param string $origin
* @param string|bool $storeAuth
*/
public function storeAuth($origin, $storeAuth)
{ {
$store = false; $store = false;
$configSource = $this->config->getAuthConfigSource(); $configSource = $this->config->getAuthConfigSource();
@ -37,7 +42,7 @@ class AuthHelper
$store = $configSource; $store = $configSource;
} elseif ($storeAuth === 'prompt') { } elseif ($storeAuth === 'prompt') {
$answer = $this->io->askAndValidate( $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) { function ($value) {
$input = strtolower(substr(trim($value), 0, 1)); $input = strtolower(substr(trim($value), 0, 1));
if (in_array($input, array('y','n'))) { if (in_array($input, array('y','n'))) {
@ -55,9 +60,192 @@ class AuthHelper
} }
if ($store) { if ($store) {
$store->addConfigSetting( $store->addConfigSetting(
'http-basic.'.$originUrl, 'http-basic.'.$origin,
$this->io->getAuthentication($originUrl) $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 $io;
private $config; private $config;
private $process; private $process;
private $remoteFilesystem; private $httpDownloader;
private $token = array(); private $token = array();
private $time; private $time;
@ -37,15 +37,15 @@ class Bitbucket
* @param IOInterface $io The IO instance * @param IOInterface $io The IO instance
* @param Config $config The composer configuration * @param Config $config The composer configuration
* @param ProcessExecutor $process Process instance, injectable for mocking * @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 * @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->io = $io;
$this->config = $config; $this->config = $config;
$this->process = $process ?: new ProcessExecutor($io); $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; $this->time = $time;
} }
@ -90,7 +90,7 @@ class Bitbucket
private function requestAccessToken($originUrl) private function requestAccessToken($originUrl)
{ {
try { 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, 'retry-auth-failure' => false,
'http' => array( 'http' => array(
'method' => 'POST', 'method' => 'POST',
@ -98,7 +98,7 @@ class Bitbucket
), ),
)); ));
$this->token = json_decode($json, true); $this->token = $response->decodeJson();
} catch (TransportException $e) { } catch (TransportException $e) {
if ($e->getCode() === 400) { if ($e->getCode() === 400) {
$this->io->writeError('<error>Invalid OAuth consumer provided.</error>'); $this->io->writeError('<error>Invalid OAuth consumer provided.</error>');

View File

@ -25,7 +25,7 @@ class GitHub
protected $io; protected $io;
protected $config; protected $config;
protected $process; protected $process;
protected $remoteFilesystem; protected $httpDownloader;
/** /**
* Constructor. * Constructor.
@ -33,14 +33,14 @@ class GitHub
* @param IOInterface $io The IO instance * @param IOInterface $io The IO instance
* @param Config $config The composer configuration * @param Config $config The composer configuration
* @param ProcessExecutor $process Process instance, injectable for mocking * @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->io = $io;
$this->config = $config; $this->config = $config;
$this->process = $process ?: new ProcessExecutor($io); $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 { try {
$apiUrl = ('github.com' === $originUrl) ? 'api.github.com/' : $originUrl . '/api/v3/'; $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, 'retry-auth-failure' => false,
)); ));
} catch (TransportException $e) { } catch (TransportException $e) {

View File

@ -26,7 +26,7 @@ class GitLab
protected $io; protected $io;
protected $config; protected $config;
protected $process; protected $process;
protected $remoteFilesystem; protected $httpDownloader;
/** /**
* Constructor. * Constructor.
@ -34,14 +34,14 @@ class GitLab
* @param IOInterface $io The IO instance * @param IOInterface $io The IO instance
* @param Config $config The composer configuration * @param Config $config The composer configuration
* @param ProcessExecutor $process Process instance, injectable for mocking * @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->io = $io;
$this->config = $config; $this->config = $config;
$this->process = $process ?: new ProcessExecutor($io); $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'); $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 $retryAuthFailure;
private $lastHeaders; private $lastHeaders;
private $storeAuth; private $storeAuth;
private $authHelper;
private $degradedMode = false; private $degradedMode = false;
private $redirects; private $redirects;
private $maxRedirects = 20; private $maxRedirects = 20;
@ -53,14 +54,15 @@ class RemoteFilesystem
* @param array $options The options * @param array $options The options
* @param bool $disableTls * @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; $this->io = $io;
// Setup TLS options // Setup TLS options
// The cafile option can be set via config.json // The cafile option can be set via config.json
if ($disableTls === false) { if ($disableTls === false) {
$this->options = $this->getTlsDefaults($options); $logger = $io instanceof LoggerInterface ? $io : null;
$this->options = StreamContextFactory::getTlsDefaults($options, $logger);
} else { } else {
$this->disableTls = true; $this->disableTls = true;
} }
@ -68,6 +70,7 @@ class RemoteFilesystem
// handle the other externally set options normally. // handle the other externally set options normally.
$this->options = array_replace_recursive($this->options, $options); $this->options = array_replace_recursive($this->options, $options);
$this->config = $config; $this->config = $config;
$this->authHelper = new AuthHelper($io, $config);
} }
/** /**
@ -146,7 +149,7 @@ class RemoteFilesystem
* @param string $name header name (case insensitive) * @param string $name header name (case insensitive)
* @return string|null * @return string|null
*/ */
public function findHeaderValue(array $headers, $name) public static function findHeaderValue(array $headers, $name)
{ {
$value = null; $value = null;
foreach ($headers as $header) { foreach ($headers as $header) {
@ -166,7 +169,7 @@ class RemoteFilesystem
* @param array $headers array of returned headers like from getLastHeaders() * @param array $headers array of returned headers like from getLastHeaders()
* @return int|null * @return int|null
*/ */
public function findStatusCode(array $headers) public static function findStatusCode(array $headers)
{ {
$value = null; $value = null;
foreach ($headers as $header) { foreach ($headers as $header) {
@ -214,27 +217,6 @@ class RemoteFilesystem
*/ */
protected function get($originUrl, $fileUrl, $additionalOptions = array(), $fileName = null, $progress = true) 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->scheme = parse_url($fileUrl, PHP_URL_SCHEME);
$this->bytesMax = 0; $this->bytesMax = 0;
$this->originUrl = $originUrl; $this->originUrl = $originUrl;
@ -246,11 +228,6 @@ class RemoteFilesystem
$this->lastHeaders = array(); $this->lastHeaders = array();
$this->redirects = 1; // The first request counts. $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; $tempAdditionalOptions = $additionalOptions;
if (isset($tempAdditionalOptions['retry-auth-failure'])) { if (isset($tempAdditionalOptions['retry-auth-failure'])) {
$this->retryAuthFailure = (bool) $tempAdditionalOptions['retry-auth-failure']; $this->retryAuthFailure = (bool) $tempAdditionalOptions['retry-auth-failure'];
@ -271,14 +248,6 @@ class RemoteFilesystem
$origFileUrl = $fileUrl; $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'])) { if (isset($options['gitlab-token'])) {
$fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['gitlab-token']; $fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['gitlab-token'];
unset($options['gitlab-token']); unset($options['gitlab-token']);
@ -399,7 +368,7 @@ class RemoteFilesystem
// check for bitbucket login page asking to authenticate // check for bitbucket login page asking to authenticate
if ($originUrl === 'bitbucket.org' if ($originUrl === 'bitbucket.org'
&& !$this->isPublicBitBucketDownload($fileUrl) && !$this->authHelper->isPublicBitBucketDownload($fileUrl)
&& substr($fileUrl, -4) === '.zip' && substr($fileUrl, -4) === '.zip'
&& (!$locationHeader || substr($locationHeader, -4) !== '.zip') && (!$locationHeader || substr($locationHeader, -4) !== '.zip')
&& $contentType && preg_match('{^text/html\b}i', $contentType) && $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); $result = $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress);
if ($this->storeAuth && $this->config) { if ($this->storeAuth && $this->config) {
$authHelper = new AuthHelper($this->io, $this->config); $this->authHelper->storeAuth($this->originUrl, $this->storeAuth);
$authHelper->storeAuth($this->originUrl, $this->storeAuth);
$this->storeAuth = false; $this->storeAuth = false;
} }
@ -649,111 +617,14 @@ class RemoteFilesystem
protected function promptAuthAndRetry($httpStatus, $reason = null, $warning = null, $headers = array()) protected function promptAuthAndRetry($httpStatus, $reason = null, $warning = null, $headers = array())
{ {
if ($this->config && in_array($this->originUrl, $this->config->get('github-domains'), true)) { $result = $this->authHelper->promptAuthIfNeeded($this->fileUrl, $this->originUrl, $httpStatus, $reason, $warning, $headers);
$gitHubUtil = new GitHub($this->io, $this->config, null);
$message = "\n";
$rateLimited = $gitHubUtil->isRateLimited($headers); $this->storeAuth = $result['storeAuth'];
if ($rateLimited) { $this->retry = $result['retry'];
$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.';
}
$message = sprintf( if ($this->retry) {
'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.', throw new TransportException('RETRY');
$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');
} }
$this->retry = true;
throw new TransportException('RETRY');
} }
protected function getOptionsForUrl($originUrl, $additionalOptions) protected function getOptionsForUrl($originUrl, $additionalOptions)
@ -813,27 +684,7 @@ class RemoteFilesystem
$headers[] = 'Connection: close'; $headers[] = 'Connection: close';
} }
if ($this->io->hasAuthentication($originUrl)) { $headers = $this->authHelper->addAuthenticationHeader($headers, $originUrl, $this->fileUrl);
$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;
}
}
$options['http']['follow_location'] = 0; $options['http']['follow_location'] = 0;
@ -891,111 +742,6 @@ class RemoteFilesystem
return false; 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. * Fetch certificate common name and fingerprint for validation of SAN.
* *
@ -1065,29 +811,4 @@ class RemoteFilesystem
return parse_url($url, PHP_URL_HOST).':'.$port; 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; namespace Composer\Util;
use Composer\Composer; use Composer\Composer;
use Composer\CaBundle\CaBundle;
use Psr\Log\LoggerInterface;
/** /**
* Allows the creation of a basic context supporting http proxy * Allows the creation of a basic context supporting http proxy
@ -39,6 +41,32 @@ final class StreamContextFactory
'max_redirects' => 20, '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 // 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']))) { 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']); $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 // enabled request_fulluri unless it is explicitly disabled
switch (parse_url($url, PHP_URL_SCHEME)) { 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'); $reqFullUriEnv = getenv('HTTP_PROXY_REQUEST_FULLURI');
if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) { if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) {
$options['http']['request_fulluri'] = true; $options['http']['request_fulluri'] = true;
} }
break; break;
case 'https': // default request_fulluri to true case 'https': // default request_fulluri to false for HTTPS
$reqFullUriEnv = getenv('HTTPS_PROXY_REQUEST_FULLURI'); $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; $options['http']['request_fulluri'] = true;
} }
break; break;
@ -115,42 +143,139 @@ final class StreamContextFactory
} }
$auth = base64_encode($auth); $auth = base64_encode($auth);
// Preserve headers if already set in default options $options['http']['header'][] = "Proxy-Authorization: Basic {$auth}";
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 = array_replace_recursive($options, $defaultOptions);
if (isset($options['http']['header'])) {
$options['http']['header'] = self::fixHttpHeaderField($options['http']['header']);
}
if (defined('HHVM_VERSION')) { if (defined('HHVM_VERSION')) {
$phpVersion = 'HHVM ' . HHVM_VERSION; $phpVersion = 'HHVM ' . HHVM_VERSION;
} else { } else {
$phpVersion = 'PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION; $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')) { if (!isset($options['http']['header']) || false === stripos(implode('', $options['http']['header']), 'user-agent')) {
$options['http']['header'][] = sprintf( $options['http']['header'][] = sprintf(
'User-Agent: Composer/%s (%s; %s; %s%s)', 'User-Agent: Composer/%s (%s; %s; %s; %s%s)',
Composer::VERSION === '@package_version@' ? 'source' : Composer::VERSION, Composer::VERSION === '@package_version@' ? Composer::SOURCE_VERSION : Composer::VERSION,
function_exists('php_uname') ? php_uname('s') : 'Unknown', function_exists('php_uname') ? php_uname('s') : 'Unknown',
function_exists('php_uname') ? php_uname('r') : 'Unknown', function_exists('php_uname') ? php_uname('r') : 'Unknown',
$phpVersion, $phpVersion,
$httpVersion,
getenv('CI') ? '; CI' : '' 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 class Url
{ {
/**
* @param Config $config
* @param string $url
* @param string $ref
* @return string the updated URL
*/
public static function updateDistReference(Config $config, $url, $ref) public static function updateDistReference(Config $config, $url, $ref)
{ {
$host = parse_url($url, PHP_URL_HOST); $host = parse_url($url, PHP_URL_HOST);
@ -52,4 +58,45 @@ class Url
return $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() public function testSetGetInstallationManager()
{ {
$composer = new Composer(); $composer = new Composer();
$manager = $this->getMockBuilder('Composer\Installer\InstallationManager')->getMock(); $manager = $this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock();
$composer->setInstallationManager($manager); $composer->setInstallationManager($manager);
$this->assertSame($manager, $composer->getInstallationManager()); $this->assertSame($manager, $composer->getInstallationManager());

View File

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

View File

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

View File

@ -56,7 +56,7 @@ class FossilDownloaderTest extends TestCase
->will($this->returnValue(null)); ->will($this->returnValue(null));
$downloader = $this->getDownloaderMock(); $downloader = $this->getDownloaderMock();
$downloader->download($packageMock, '/path'); $downloader->install($packageMock, '/path');
} }
public function testDownload() public function testDownload()
@ -89,7 +89,7 @@ class FossilDownloaderTest extends TestCase
->will($this->returnValue(0)); ->will($this->returnValue(0));
$downloader = $this->getDownloaderMock(null, null, $processExecutor); $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)); ->will($this->returnValue(null));
$downloader = $this->getDownloaderMock(); $downloader = $this->getDownloaderMock();
$downloader->download($packageMock, '/path'); $downloader->install($packageMock, '/path');
} }
public function testDownload() public function testDownload()
@ -130,7 +130,7 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue(0)); ->will($this->returnValue(0));
$downloader = $this->getDownloaderMock(null, null, $processExecutor); $downloader = $this->getDownloaderMock(null, null, $processExecutor);
$downloader->download($packageMock, 'composerPath'); $downloader->install($packageMock, 'composerPath');
} }
public function testDownloadWithCache() public function testDownloadWithCache()
@ -195,7 +195,7 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue(0)); ->will($this->returnValue(0));
$downloader = $this->getDownloaderMock(null, $config, $processExecutor); $downloader = $this->getDownloaderMock(null, $config, $processExecutor);
$downloader->download($packageMock, 'composerPath'); $downloader->install($packageMock, 'composerPath');
@rmdir($cachePath); @rmdir($cachePath);
} }
@ -265,7 +265,7 @@ class GitDownloaderTest extends TestCase
->will($this->returnValue(0)); ->will($this->returnValue(0));
$downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor);
$downloader->download($packageMock, 'composerPath'); $downloader->install($packageMock, 'composerPath');
} }
public function pushUrlProvider() public function pushUrlProvider()
@ -329,7 +329,7 @@ class GitDownloaderTest extends TestCase
$config->merge(array('config' => array('github-protocols' => $protocols))); $config->merge(array('config' => array('github-protocols' => $protocols)));
$downloader = $this->getDownloaderMock(null, $config, $processExecutor); $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)); ->will($this->returnValue(1));
$downloader = $this->getDownloaderMock(null, null, $processExecutor); $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)); ->will($this->returnValue(null));
$downloader = $this->getDownloaderMock(); $downloader = $this->getDownloaderMock();
$downloader->download($packageMock, '/path'); $downloader->install($packageMock, '/path');
} }
public function testDownload() public function testDownload()
@ -83,7 +83,7 @@ class HgDownloaderTest extends TestCase
->will($this->returnValue(0)); ->will($this->returnValue(0));
$downloader = $this->getDownloaderMock(null, null, $processExecutor); $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\Repository\VcsRepository;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Test\TestCase; use Composer\Test\TestCase;
use Composer\Factory;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
/** /**
@ -96,7 +97,7 @@ class PerforceDownloaderTest extends TestCase
{ {
$repository = $this->getMockBuilder('Composer\Repository\VcsRepository') $repository = $this->getMockBuilder('Composer\Repository\VcsRepository')
->setMethods(array('getRepoConfig')) ->setMethods(array('getRepoConfig'))
->setConstructorArgs(array($repoConfig, $io, $config)) ->setConstructorArgs(array($repoConfig, $io, $config, Factory::createHttpDownloader($io, $config)))
->getMock(); ->getMock();
$repository->expects($this->any())->method('getRepoConfig')->will($this->returnValue($repoConfig)); $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(5))->method('syncCodeBase')->with($label);
$perforce->expects($this->at(6))->method('cleanupClientSpec'); $perforce->expects($this->at(6))->method('cleanupClientSpec');
$this->downloader->setPerforce($perforce); $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(5))->method('syncCodeBase')->with($label);
$perforce->expects($this->at(6))->method('cleanupClientSpec'); $perforce->expects($this->at(6))->method('cleanupClientSpec');
$this->downloader->setPerforce($perforce); $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\Test\TestCase;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Platform; use Composer\Util\Platform;
use Composer\Util\RemoteFilesystem; use Composer\Util\Loop;
use Composer\Util\HttpDownloader;
class XzDownloaderTest extends TestCase class XzDownloaderTest extends TestCase
{ {
@ -66,10 +67,14 @@ class XzDownloaderTest extends TestCase
->method('get') ->method('get')
->with('vendor-dir') ->with('vendor-dir')
->will($this->returnValue($this->testDir)); ->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 { 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'); $this->fail('Download of invalid tarball should throw an exception');
} catch (\RuntimeException $e) { } catch (\RuntimeException $e) {
$this->assertRegexp('/(File format not recognized|Unrecognized archive format)/i', $e->getMessage()); $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\Package\PackageInterface;
use Composer\Test\TestCase; use Composer\Test\TestCase;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\HttpDownloader;
use Composer\Util\Loop;
class ZipDownloaderTest extends TestCase class ZipDownloaderTest extends TestCase
{ {
@ -26,12 +28,16 @@ class ZipDownloaderTest extends TestCase
private $prophet; private $prophet;
private $io; private $io;
private $config; private $config;
private $package;
public function setUp() public function setUp()
{ {
$this->testDir = $this->getUniqueTmpDirectory(); $this->testDir = $this->getUniqueTmpDirectory();
$this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
$this->config = $this->getMockBuilder('Composer\Config')->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() public function tearDown()
@ -64,42 +70,33 @@ class ZipDownloaderTest extends TestCase
} }
$this->config->expects($this->at(0)) $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') ->method('get')
->with('vendor-dir') ->with('vendor-dir')
->will($this->returnValue($this->testDir)); ->will($this->returnValue($this->testDir));
$packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $this->package->expects($this->any())
$packageMock->expects($this->any())
->method('getDistUrl') ->method('getDistUrl')
->will($this->returnValue($distUrl = 'file://'.__FILE__)) ->will($this->returnValue($distUrl = 'file://'.__FILE__))
; ;
$packageMock->expects($this->any()) $this->package->expects($this->any())
->method('getDistUrls') ->method('getDistUrls')
->will($this->returnValue(array($distUrl))) ->will($this->returnValue(array($distUrl)))
; ;
$packageMock->expects($this->atLeastOnce()) $this->package->expects($this->atLeastOnce())
->method('getTransportOptions') ->method('getTransportOptions')
->will($this->returnValue(array())) ->will($this->returnValue(array()))
; ;
$downloader = new ZipDownloader($this->io, $this->config); $downloader = new ZipDownloader($this->io, $this->config, $this->httpDownloader);
$this->setPrivateProperty('hasSystemUnzip', false); $this->setPrivateProperty('hasSystemUnzip', false);
try { 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'); $this->fail('Download of invalid zip files should throw an exception');
} catch (\Exception $e) { } catch (\Exception $e) {
$this->assertContains('is not a zip archive', $e->getMessage()); $this->assertContains('is not a zip archive', $e->getMessage());
@ -118,8 +115,7 @@ class ZipDownloaderTest extends TestCase
$this->setPrivateProperty('hasSystemUnzip', false); $this->setPrivateProperty('hasSystemUnzip', false);
$this->setPrivateProperty('hasZipArchive', true); $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 = $this->getMockBuilder('ZipArchive')->getMock();
$zipArchive->expects($this->at(0)) $zipArchive->expects($this->at(0))
->method('open') ->method('open')
@ -129,7 +125,7 @@ class ZipDownloaderTest extends TestCase
->will($this->returnValue(false)); ->will($this->returnValue(false));
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); $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('hasSystemUnzip', false);
$this->setPrivateProperty('hasZipArchive', true); $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 = $this->getMockBuilder('ZipArchive')->getMock();
$zipArchive->expects($this->at(0)) $zipArchive->expects($this->at(0))
->method('open') ->method('open')
@ -155,7 +150,7 @@ class ZipDownloaderTest extends TestCase
->will($this->throwException(new \ErrorException('Not a directory'))); ->will($this->throwException(new \ErrorException('Not a directory')));
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); $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('hasSystemUnzip', false);
$this->setPrivateProperty('hasZipArchive', true); $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 = $this->getMockBuilder('ZipArchive')->getMock();
$zipArchive->expects($this->at(0)) $zipArchive->expects($this->at(0))
->method('open') ->method('open')
@ -180,7 +174,7 @@ class ZipDownloaderTest extends TestCase
->will($this->returnValue(true)); ->will($this->returnValue(true));
$this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); $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') ->method('execute')
->will($this->returnValue(1)); ->will($this->returnValue(1));
$downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor); $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor);
$downloader->extract('testfile.zip', 'vendor/dir'); $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
} }
public function testSystemUnzipOnlyGood() public function testSystemUnzipOnlyGood()
@ -217,8 +211,8 @@ class ZipDownloaderTest extends TestCase
->method('execute') ->method('execute')
->will($this->returnValue(0)); ->will($this->returnValue(0));
$downloader = new MockedZipDownloader($this->io, $this->config, null, null, $processExecutor); $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor);
$downloader->extract('testfile.zip', 'vendor/dir'); $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
} }
public function testNonWindowsFallbackGood() public function testNonWindowsFallbackGood()
@ -244,9 +238,9 @@ class ZipDownloaderTest extends TestCase
->method('extractTo') ->method('extractTo')
->will($this->returnValue(true)); ->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); $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') ->method('extractTo')
->will($this->returnValue(false)); ->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); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader);
$downloader->extract('testfile.zip', 'vendor/dir'); $downloader->extract($this->package, 'testfile.zip', 'vendor/dir');
} }
public function testWindowsFallbackGood() public function testWindowsFallbackGood()
@ -304,9 +298,9 @@ class ZipDownloaderTest extends TestCase
->method('extractTo') ->method('extractTo')
->will($this->returnValue(false)); ->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); $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') ->method('extractTo')
->will($this->returnValue(false)); ->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); $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; 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->setPackage($package);
$composer->setRepositoryManager($this->getRepositoryManagerMockForDevModePassingTest()); $composer->setRepositoryManager($this->getRepositoryManagerMockForDevModePassingTest());
$composer->setInstallationManager($this->getMockBuilder('Composer\Installer\InstallationManager')->getMock()); $composer->setInstallationManager($this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock());
$dispatcher = new EventDispatcher( $dispatcher = new EventDispatcher(
$composer, $composer,

View File

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

View File

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

View File

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

View File

@ -63,7 +63,9 @@ class InstallerTest extends TestCase
->getMock(); ->getMock();
$config = $this->getMockBuilder('Composer\Config')->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()); $repositoryManager->setLocalRepository(new InstalledArrayRepository());
if (!is_array($repositories)) { if (!is_array($repositories)) {
@ -76,7 +78,6 @@ class InstallerTest extends TestCase
$locker = $this->getMockBuilder('Composer\Package\Locker')->disableOriginalConstructor()->getMock(); $locker = $this->getMockBuilder('Composer\Package\Locker')->disableOriginalConstructor()->getMock();
$installationManager = new InstallationManagerMock(); $installationManager = new InstallationManagerMock();
$eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock();
$autoloadGenerator = $this->getMockBuilder('Composer\Autoload\AutoloadGenerator')->disableOriginalConstructor()->getMock(); $autoloadGenerator = $this->getMockBuilder('Composer\Autoload\AutoloadGenerator')->disableOriginalConstructor()->getMock();
$installer = new Installer($io, $config, clone $rootPackage, $downloadManager, $repositoryManager, $locker, $installationManager, $eventDispatcher, $autoloadGenerator); $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\Installer;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Test\TestCase; use Composer\Test\TestCase;
use Composer\Util\Loop;
class FactoryMock extends Factory 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) protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io)

View File

@ -12,13 +12,11 @@
namespace Composer\Test\Mock; namespace Composer\Test\Mock;
use Composer\Util\RemoteFilesystem; use Composer\Util\HttpDownloader;
use Composer\Util\Http\Response;
use Composer\Downloader\TransportException; use Composer\Downloader\TransportException;
/** class HttpDownloaderMock extends HttpDownloader
* Remote filesystem mock
*/
class RemoteFilesystemMock extends RemoteFilesystem
{ {
protected $contentMap; protected $contentMap;
@ -30,10 +28,10 @@ class RemoteFilesystemMock extends RemoteFilesystem
$this->contentMap = $contentMap; $this->contentMap = $contentMap;
} }
public function getContents($originUrl, $fileUrl, $progress = true, $options = array()) public function get($fileUrl, $options = array())
{ {
if (!empty($this->contentMap[$fileUrl])) { 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); 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\Repository\InstalledRepositoryInterface;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation; use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation;
@ -29,6 +30,18 @@ class InstallationManagerMock extends InstallationManager
private $uninstalled = array(); private $uninstalled = array();
private $trace = 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) public function getInstallPath(PackageInterface $package)
{ {
return ''; return '';

View File

@ -12,9 +12,12 @@
namespace Composer\Test\Package\Archiver; namespace Composer\Test\Package\Archiver;
use Composer\IO\NullIO;
use Composer\Factory; use Composer\Factory;
use Composer\Package\Archiver\ArchiveManager; use Composer\Package\Archiver\ArchiveManager;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Util\Loop;
use Composer\Test\Mock\FactoryMock;
class ArchiveManagerTest extends ArchiverTest class ArchiveManagerTest extends ArchiverTest
{ {
@ -30,7 +33,13 @@ class ArchiveManagerTest extends ArchiverTest
parent::setUp(); parent::setUp();
$factory = new Factory(); $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'; $this->targetDir = $this->testDir.'/composer_archiver_tests';
} }

View File

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

View File

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

View File

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

View File

@ -22,19 +22,19 @@ use Composer\Semver\VersionParser;
use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\Constraint;
use Composer\Package\Link; use Composer\Package\Link;
use Composer\Package\CompletePackage; use Composer\Package\CompletePackage;
use Composer\Test\Mock\RemoteFilesystemMock; use Composer\Test\Mock\HttpDownloaderMock;
class ChannelReaderTest extends TestCase class ChannelReaderTest extends TestCase
{ {
public function testShouldBuildPackagesFromPearSchema() 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://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/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'), '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/'); $channelInfo = $reader->read('http://pear.net/');
$packages = $channelInfo->getPackages(); $packages = $channelInfo->getPackages();
@ -50,17 +50,21 @@ class ChannelReaderTest extends TestCase
public function testShouldSelectCorrectReader() 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://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/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/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/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://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/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'), '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.0.net/');
$reader->read('http://pear.1.1.net/'); $reader->read('http://pear.1.1.net/');

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