Browse Source

Merge pull request #7904 from Seldaek/multi-curl

Parallel downloads
Nils Adermann 6 years ago
parent
commit
9f18b54cb6
100 changed files with 3083 additions and 1394 deletions
  1. 2 1
      composer.json
  2. 45 1
      composer.lock
  3. 5 0
      doc/03-cli.md
  4. 2 2
      doc/articles/plugins.md
  5. 1 1
      doc/articles/scripts.md
  6. 4 2
      src/Composer/Command/ArchiveCommand.php
  7. 8 18
      src/Composer/Command/CreateProjectCommand.php
  8. 14 15
      src/Composer/Command/DiagnoseCommand.php
  9. 0 1
      src/Composer/Command/InstallCommand.php
  10. 0 1
      src/Composer/Command/RemoveCommand.php
  11. 0 1
      src/Composer/Command/RequireCommand.php
  12. 4 4
      src/Composer/Command/SelfUpdateCommand.php
  13. 3 3
      src/Composer/Command/ShowCommand.php
  14. 1 1
      src/Composer/Command/StatusCommand.php
  15. 0 2
      src/Composer/Command/UpdateCommand.php
  16. 1 0
      src/Composer/Compiler.php
  17. 1 0
      src/Composer/Composer.php
  18. 1 0
      src/Composer/DependencyResolver/Solver.php
  19. 52 46
      src/Composer/Downloader/ArchiveDownloader.php
  20. 139 81
      src/Composer/Downloader/DownloadManager.php
  21. 9 9
      src/Composer/Downloader/DownloaderInterface.php
  22. 130 95
      src/Composer/Downloader/FileDownloader.php
  23. 1 1
      src/Composer/Downloader/FossilDownloader.php
  24. 1 1
      src/Composer/Downloader/GitDownloader.php
  25. 6 13
      src/Composer/Downloader/GzipDownloader.php
  26. 1 1
      src/Composer/Downloader/HgDownloader.php
  27. 9 0
      src/Composer/Downloader/PathDownloader.php
  28. 1 1
      src/Composer/Downloader/PerforceDownloader.php
  29. 3 1
      src/Composer/Downloader/PharDownloader.php
  30. 5 4
      src/Composer/Downloader/RarDownloader.php
  31. 1 1
      src/Composer/Downloader/SvnDownloader.php
  32. 3 1
      src/Composer/Downloader/TarDownloader.php
  33. 10 11
      src/Composer/Downloader/VcsDownloader.php
  34. 4 12
      src/Composer/Downloader/XzDownloader.php
  35. 4 4
      src/Composer/Downloader/ZipDownloader.php
  36. 30 34
      src/Composer/Factory.php
  37. 26 1
      src/Composer/Installer/InstallationManager.php
  38. 10 0
      src/Composer/Installer/InstallerInterface.php
  39. 9 1
      src/Composer/Installer/LibraryInstaller.php
  40. 8 0
      src/Composer/Installer/MetapackageInstaller.php
  41. 7 0
      src/Composer/Installer/NoopInstaller.php
  42. 9 1
      src/Composer/Installer/PluginInstaller.php
  43. 11 2
      src/Composer/Installer/ProjectInstaller.php
  44. 10 10
      src/Composer/Json/JsonFile.php
  45. 7 2
      src/Composer/Package/Archiver/ArchiveManager.php
  46. 120 33
      src/Composer/Package/Loader/ArrayLoader.php
  47. 2 1
      src/Composer/Package/Version/VersionGuesser.php
  48. 9 21
      src/Composer/Plugin/PreFileDownloadEvent.php
  49. 527 205
      src/Composer/Repository/ComposerRepository.php
  50. 10 6
      src/Composer/Repository/Pear/BaseChannelReader.php
  51. 5 5
      src/Composer/Repository/Pear/ChannelReader.php
  52. 3 2
      src/Composer/Repository/Pear/ChannelRest10Reader.php
  53. 4 2
      src/Composer/Repository/Pear/ChannelRest11Reader.php
  54. 5 5
      src/Composer/Repository/PearRepository.php
  55. 4 0
      src/Composer/Repository/PlatformRepository.php
  56. 7 7
      src/Composer/Repository/RepositoryFactory.php
  57. 6 5
      src/Composer/Repository/RepositoryInterface.php
  58. 6 6
      src/Composer/Repository/RepositoryManager.php
  59. 15 10
      src/Composer/Repository/Vcs/BitbucketDriver.php
  60. 2 2
      src/Composer/Repository/Vcs/GitBitbucketDriver.php
  61. 30 25
      src/Composer/Repository/Vcs/GitHubDriver.php
  62. 30 27
      src/Composer/Repository/Vcs/GitLabDriver.php
  63. 2 2
      src/Composer/Repository/Vcs/HgBitbucketDriver.php
  64. 10 9
      src/Composer/Repository/Vcs/VcsDriver.php
  65. 10 4
      src/Composer/Repository/VcsRepository.php
  66. 5 5
      src/Composer/SelfUpdate/Versions.php
  67. 192 4
      src/Composer/Util/AuthHelper.php
  68. 6 6
      src/Composer/Util/Bitbucket.php
  69. 5 5
      src/Composer/Util/GitHub.php
  70. 6 6
      src/Composer/Util/GitLab.php
  71. 463 0
      src/Composer/Util/Http/CurlDownloader.php
  72. 94 0
      src/Composer/Util/Http/Response.php
  73. 318 0
      src/Composer/Util/HttpDownloader.php
  74. 47 0
      src/Composer/Util/Loop.php
  75. 15 294
      src/Composer/Util/RemoteFilesystem.php
  76. 146 21
      src/Composer/Util/StreamContextFactory.php
  77. 47 0
      src/Composer/Util/Url.php
  78. 1 1
      tests/Composer/Test/ComposerTest.php
  79. 6 2
      tests/Composer/Test/Downloader/ArchiveDownloaderTest.php
  80. 123 150
      tests/Composer/Test/Downloader/DownloadManagerTest.php
  81. 33 12
      tests/Composer/Test/Downloader/FileDownloaderTest.php
  82. 2 2
      tests/Composer/Test/Downloader/FossilDownloaderTest.php
  83. 6 6
      tests/Composer/Test/Downloader/GitDownloaderTest.php
  84. 2 2
      tests/Composer/Test/Downloader/HgDownloaderTest.php
  85. 4 3
      tests/Composer/Test/Downloader/PerforceDownloaderTest.php
  86. 8 3
      tests/Composer/Test/Downloader/XzDownloaderTest.php
  87. 40 41
      tests/Composer/Test/Downloader/ZipDownloaderTest.php
  88. 1 1
      tests/Composer/Test/EventDispatcher/EventDispatcherTest.php
  89. 1 1
      tests/Composer/Test/FactoryTest.php
  90. 26 15
      tests/Composer/Test/Installer/InstallationManagerTest.php
  91. 1 1
      tests/Composer/Test/Installer/LibraryInstallerTest.php
  92. 3 2
      tests/Composer/Test/InstallerTest.php
  93. 3 2
      tests/Composer/Test/Mock/FactoryMock.php
  94. 5 7
      tests/Composer/Test/Mock/HttpDownloaderMock.php
  95. 13 0
      tests/Composer/Test/Mock/InstallationManagerMock.php
  96. 10 1
      tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php
  97. 1 1
      tests/Composer/Test/Plugin/PluginInstallerTest.php
  98. 35 31
      tests/Composer/Test/Repository/ComposerRepositoryTest.php
  99. 1 1
      tests/Composer/Test/Repository/PathRepositoryTest.php
  100. 9 5
      tests/Composer/Test/Repository/Pear/ChannelReaderTest.php

+ 2 - 1
composer.json

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

+ 45 - 1
composer.lock

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

+ 5 - 0
doc/03-cli.md

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

+ 2 - 2
doc/articles/plugins.md

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

+ 1 - 1
doc/articles/scripts.md

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

+ 4 - 2
src/Composer/Command/ArchiveCommand.php

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

+ 8 - 18
src/Composer/Command/CreateProjectCommand.php

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

+ 14 - 15
src/Composer/Command/DiagnoseCommand.php

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

+ 0 - 1
src/Composer/Command/InstallCommand.php

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

+ 0 - 1
src/Composer/Command/RemoveCommand.php

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

+ 0 - 1
src/Composer/Command/RequireCommand.php

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

+ 4 - 4
src/Composer/Command/SelfUpdateCommand.php

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

+ 3 - 3
src/Composer/Command/ShowCommand.php

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

+ 1 - 1
src/Composer/Command/StatusCommand.php

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

+ 0 - 2
src/Composer/Command/UpdateCommand.php

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

+ 1 - 0
src/Composer/Compiler.php

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

+ 1 - 0
src/Composer/Composer.php

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

+ 1 - 0
src/Composer/DependencyResolver/Solver.php

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

+ 52 - 46
src/Composer/Downloader/ArchiveDownloader.php

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

+ 139 - 81
src/Composer/Downloader/DownloadManager.php

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

+ 9 - 9
src/Composer/Downloader/DownloaderInterface.php

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

+ 130 - 95
src/Composer/Downloader/FileDownloader.php

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

+ 1 - 1
src/Composer/Downloader/FossilDownloader.php

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

+ 1 - 1
src/Composer/Downloader/GitDownloader.php

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

+ 6 - 13
src/Composer/Downloader/GzipDownloader.php

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

+ 1 - 1
src/Composer/Downloader/HgDownloader.php

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

+ 9 - 0
src/Composer/Downloader/PathDownloader.php

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

+ 1 - 1
src/Composer/Downloader/PerforceDownloader.php

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

+ 3 - 1
src/Composer/Downloader/PharDownloader.php

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

+ 5 - 4
src/Composer/Downloader/RarDownloader.php

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

+ 1 - 1
src/Composer/Downloader/SvnDownloader.php

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

+ 3 - 1
src/Composer/Downloader/TarDownloader.php

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

+ 10 - 11
src/Composer/Downloader/VcsDownloader.php

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

+ 4 - 12
src/Composer/Downloader/XzDownloader.php

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

+ 4 - 4
src/Composer/Downloader/ZipDownloader.php

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

+ 30 - 34
src/Composer/Factory.php

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

+ 26 - 1
src/Composer/Installer/InstallationManager.php

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

+ 10 - 0
src/Composer/Installer/InstallerInterface.php

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

+ 9 - 1
src/Composer/Installer/LibraryInstaller.php

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

+ 8 - 0
src/Composer/Installer/MetapackageInstaller.php

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

+ 7 - 0
src/Composer/Installer/NoopInstaller.php

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

+ 9 - 1
src/Composer/Installer/PluginInstaller.php

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

+ 11 - 2
src/Composer/Installer/ProjectInstaller.php

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

+ 10 - 10
src/Composer/Json/JsonFile.php

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

+ 7 - 2
src/Composer/Package/Archiver/ArchiveManager.php

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

+ 120 - 33
src/Composer/Package/Loader/ArrayLoader.php

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

+ 2 - 1
src/Composer/Package/Version/VersionGuesser.php

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

+ 9 - 21
src/Composer/Plugin/PreFileDownloadEvent.php

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

File diff suppressed because it is too large
+ 527 - 205
src/Composer/Repository/ComposerRepository.php


+ 10 - 6
src/Composer/Repository/Pear/BaseChannelReader.php

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

+ 5 - 5
src/Composer/Repository/Pear/ChannelReader.php

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

+ 3 - 2
src/Composer/Repository/Pear/ChannelRest10Reader.php

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

+ 4 - 2
src/Composer/Repository/Pear/ChannelRest11Reader.php

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

+ 5 - 5
src/Composer/Repository/PearRepository.php

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

+ 4 - 0
src/Composer/Repository/PlatformRepository.php

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

+ 7 - 7
src/Composer/Repository/RepositoryFactory.php

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

+ 6 - 5
src/Composer/Repository/RepositoryInterface.php

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

+ 6 - 6
src/Composer/Repository/RepositoryManager.php

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

+ 15 - 10
src/Composer/Repository/Vcs/BitbucketDriver.php

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

+ 2 - 2
src/Composer/Repository/Vcs/GitBitbucketDriver.php

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

+ 30 - 25
src/Composer/Repository/Vcs/GitHubDriver.php

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

+ 30 - 27
src/Composer/Repository/Vcs/GitLabDriver.php

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

+ 2 - 2
src/Composer/Repository/Vcs/HgBitbucketDriver.php

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

+ 10 - 9
src/Composer/Repository/Vcs/VcsDriver.php

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

+ 10 - 4
src/Composer/Repository/VcsRepository.php

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

+ 5 - 5
src/Composer/SelfUpdate/Versions.php

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

+ 192 - 4
src/Composer/Util/AuthHelper.php

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

+ 6 - 6
src/Composer/Util/Bitbucket.php

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

+ 5 - 5
src/Composer/Util/GitHub.php

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

+ 6 - 6
src/Composer/Util/GitLab.php

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

+ 463 - 0
src/Composer/Util/Http/CurlDownloader.php

@@ -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
+            );
+        }
+    }
+}

+ 94 - 0
src/Composer/Util/Http/Response.php

@@ -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;
+    }
+}

+ 318 - 0
src/Composer/Util/HttpDownloader.php

@@ -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;
+    }
+}

+ 47 - 0
src/Composer/Util/Loop.php

@@ -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;
+        }
+    }
+}

+ 15 - 294
src/Composer/Util/RemoteFilesystem.php

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

+ 146 - 21
src/Composer/Util/StreamContextFactory.php

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

+ 47 - 0
src/Composer/Util/Url.php

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

+ 1 - 1
tests/Composer/Test/ComposerTest.php

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

+ 6 - 2
tests/Composer/Test/Downloader/ArchiveDownloaderTest.php

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

+ 123 - 150
tests/Composer/Test/Downloader/DownloadManagerTest.php

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

+ 33 - 12
tests/Composer/Test/Downloader/FileDownloaderTest.php

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

+ 2 - 2
tests/Composer/Test/Downloader/FossilDownloaderTest.php

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

+ 6 - 6
tests/Composer/Test/Downloader/GitDownloaderTest.php

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

+ 2 - 2
tests/Composer/Test/Downloader/HgDownloaderTest.php

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

+ 4 - 3
tests/Composer/Test/Downloader/PerforceDownloaderTest.php

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

+ 8 - 3
tests/Composer/Test/Downloader/XzDownloaderTest.php

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

+ 40 - 41
tests/Composer/Test/Downloader/ZipDownloaderTest.php

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

+ 1 - 1
tests/Composer/Test/EventDispatcher/EventDispatcherTest.php

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

+ 1 - 1
tests/Composer/Test/FactoryTest.php

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

+ 26 - 15
tests/Composer/Test/Installer/InstallationManagerTest.php

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

+ 1 - 1
tests/Composer/Test/Installer/LibraryInstallerTest.php

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

+ 3 - 2
tests/Composer/Test/InstallerTest.php

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

+ 3 - 2
tests/Composer/Test/Mock/FactoryMock.php

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

+ 5 - 7
tests/Composer/Test/Mock/RemoteFilesystemMock.php → tests/Composer/Test/Mock/HttpDownloaderMock.php

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

+ 13 - 0
tests/Composer/Test/Mock/InstallationManagerMock.php

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

+ 10 - 1
tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php

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

+ 1 - 1
tests/Composer/Test/Plugin/PluginInstallerTest.php

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

+ 35 - 31
tests/Composer/Test/Repository/ComposerRepositoryTest.php

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

+ 1 - 1
tests/Composer/Test/Repository/PathRepositoryTest.php

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

+ 9 - 5
tests/Composer/Test/Repository/Pear/ChannelReaderTest.php

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

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