Parcourir la source

Merge pull request #7904 from Seldaek/multi-curl

Parallel downloads
Nils Adermann il y a 6 ans
Parent
commit
9f18b54cb6
100 fichiers modifiés avec 3083 ajouts et 1394 suppressions
  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/console": "^2.7 || ^3.0 || ^4.0",
         "symfony/filesystem": "^2.7 || ^3.0 || ^4.0",
         "symfony/filesystem": "^2.7 || ^3.0 || ^4.0",
         "symfony/finder": "^2.7 || ^3.0 || ^4.0",
         "symfony/finder": "^2.7 || ^3.0 || ^4.0",
-        "symfony/process": "^2.7 || ^3.0 || ^4.0"
+        "symfony/process": "^2.7 || ^3.0 || ^4.0",
+        "react/promise": "^1.2 || ^2.7"
     },
     },
     "conflict": {
     "conflict": {
         "symfony/console": "2.8.38"
         "symfony/console": "2.8.38"

+ 45 - 1
composer.lock

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

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

+ 2 - 2
doc/articles/plugins.md

@@ -176,8 +176,8 @@ class AwsPlugin implements PluginInterface, EventSubscriberInterface
 
 
         if ($protocol === 's3') {
         if ($protocol === 's3') {
             $awsClient = new AwsClient($this->io, $this->composer->getConfig());
             $awsClient = new AwsClient($this->io, $this->composer->getConfig());
-            $s3RemoteFilesystem = new S3RemoteFilesystem($this->io, $event->getRemoteFilesystem()->getOptions(), $awsClient);
-            $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
 - **command**: occurs before any Composer Command is executed on the CLI. It
   provides you with access to the input and output objects of the program.
   provides you with access to the input and output objects of the program.
 - **pre-file-download**: occurs before files are downloaded and allows
 - **pre-file-download**: occurs before files are downloaded and allows
-  you to manipulate the `RemoteFilesystem` object prior to downloading files
+  you to manipulate the `HttpDownloader` object prior to downloading files
   based on the URL to be downloaded.
   based on the URL to be downloaded.
 - **pre-command-run**: occurs before a command is executed and allows you to
 - **pre-command-run**: occurs before a command is executed and allows you to
   manipulate the `InputInterface` object's options and arguments to tweak
   manipulate the `InputInterface` object's options and arguments to tweak

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 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);
         $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output);
         $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);
         $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/ca-bundle/')
             ->in(__DIR__.'/../../vendor/composer/xdebug-handler/')
             ->in(__DIR__.'/../../vendor/composer/xdebug-handler/')
             ->in(__DIR__.'/../../vendor/psr/')
             ->in(__DIR__.'/../../vendor/psr/')
+            ->in(__DIR__.'/../../vendor/react/')
             ->sort($finderSort)
             ->sort($finderSort)
         ;
         ;
 
 

+ 1 - 0
src/Composer/Composer.php

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

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

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

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

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

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

@@ -15,6 +15,7 @@ namespace Composer\Downloader;
 use Composer\Package\PackageInterface;
 use Composer\Package\PackageInterface;
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
 use Composer\Util\Filesystem;
 use Composer\Util\Filesystem;
+use React\Promise\PromiseInterface;
 
 
 /**
 /**
  * Downloaders manager.
  * Downloaders manager.
@@ -24,6 +25,7 @@ use Composer\Util\Filesystem;
 class DownloadManager
 class DownloadManager
 {
 {
     private $io;
     private $io;
+    private $httpDownloader;
     private $preferDist = false;
     private $preferDist = false;
     private $preferSource = false;
     private $preferSource = false;
     private $packagePreferences = array();
     private $packagePreferences = array();
@@ -33,9 +35,9 @@ class DownloadManager
     /**
     /**
      * Initializes download manager.
      * Initializes download manager.
      *
      *
-     * @param IOInterface     $io           The Input Output Interface
-     * @param 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)
     public function __construct(IOInterface $io, $preferSource = false, Filesystem $filesystem = null)
     {
     {
@@ -83,22 +85,6 @@ class DownloadManager
         return $this;
         return $this;
     }
     }
 
 
-    /**
-     * Sets whether to output download progress information for all registered
-     * downloaders
-     *
-     * @param  bool            $outputProgress
-     * @return DownloadManager
-     */
-    public function setOutputProgress($outputProgress)
-    {
-        foreach ($this->downloaders as $downloader) {
-            $downloader->setOutputProgress($outputProgress);
-        }
-
-        return $this;
-    }
-
     /**
     /**
      * Sets installer downloader for a specific installation type.
      * Sets installer downloader for a specific installation type.
      *
      *
@@ -140,7 +126,7 @@ class DownloadManager
      *                                           wrong type
      *                                           wrong type
      * @return DownloaderInterface|null
      * @return DownloaderInterface|null
      */
      */
-    public function getDownloaderForInstalledPackage(PackageInterface $package)
+    public function getDownloaderForPackage(PackageInterface $package)
     {
     {
         $installationSource = $package->getInstallationSource();
         $installationSource = $package->getInstallationSource();
 
 
@@ -154,7 +140,7 @@ class DownloadManager
             $downloader = $this->getDownloader($package->getSourceType());
             $downloader = $this->getDownloader($package->getSourceType());
         } else {
         } else {
             throw new \InvalidArgumentException(
             throw new \InvalidArgumentException(
-                'Package '.$package.' seems not been installed properly'
+                'Package '.$package.' does not have an installation source set'
             );
             );
         }
         }
 
 
@@ -171,63 +157,95 @@ class DownloadManager
         return $downloader;
         return $downloader;
     }
     }
 
 
+    public function getDownloaderType(DownloaderInterface $downloader)
+    {
+        return array_search($downloader, $this->downloaders);
+    }
+
     /**
     /**
      * Downloads package into target dir.
      * Downloads package into target dir.
      *
      *
      * @param PackageInterface $package      package instance
      * @param PackageInterface $package      package instance
      * @param string           $targetDir    target dir
      * @param string           $targetDir    target dir
-     * @param bool             $preferSource prefer installation from source
+     * @param PackageInterface $prevPackage  previous package instance in case of updates
      *
      *
+     * @return PromiseInterface
      * @throws \InvalidArgumentException if package have no urls to download from
      * @throws \InvalidArgumentException if package have no urls to download from
      * @throws \RuntimeException
      * @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);
             $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)
     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;
             return;
         }
         }
 
 
+        $initialType = $this->getDownloaderType($initialDownloader);
+        $targetType = $this->getDownloaderType($downloader);
         if ($initialType === $targetType) {
         if ($initialType === $targetType) {
-            $target->setInstallationSource($installationSource);
             try {
             try {
                 $downloader->update($initial, $target, $targetDir);
                 $downloader->update($initial, $target, $targetDir);
 
 
@@ -282,8 +292,12 @@ class DownloadManager
             }
             }
         }
         }
 
 
-        $downloader->remove($initial, $targetDir);
-        $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)
     public function remove(PackageInterface $package, $targetDir)
     {
     {
-        $downloader = $this->getDownloaderForInstalledPackage($package);
+        $downloader = $this->getDownloaderForPackage($package);
         if ($downloader) {
         if ($downloader) {
             $downloader->remove($package, $targetDir);
             $downloader->remove($package, $targetDir);
         }
         }
@@ -322,4 +336,48 @@ class DownloadManager
 
 
         return $package->isDev() ? 'source' : 'dist';
         return $package->isDev() ? 'source' : 'dist';
     }
     }
+
+    /**
+     * @return string[]
+     */
+    private function getAvailableSources(PackageInterface $package, PackageInterface $prevPackage = null)
+    {
+        $sourceType = $package->getSourceType();
+        $distType = $package->getDistType();
+
+        // add source before dist by default
+        $sources = array();
+        if ($sourceType) {
+            $sources[] = 'source';
+        }
+        if ($distType) {
+            $sources[] = 'dist';
+        }
+
+        if (empty($sources)) {
+            throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified');
+        }
+
+        if (
+            $prevPackage
+            // if we are updating, we want to keep the same source as the previously installed package (if available in the new one)
+            && in_array($prevPackage->getInstallationSource(), $sources, true)
+            // unless the previous package was stable dist (by default) and the new package is dev, then we allow the new default to take over
+            && !(!$prevPackage->isDev() && $prevPackage->getInstallationSource() === 'dist' && $package->isDev())
+        ) {
+            $prevSource = $prevPackage->getInstallationSource();
+            usort($sources, function ($a, $b) use ($prevSource) {
+                return $a === $prevSource ? -1 : 1;
+            });
+
+            return $sources;
+        }
+
+        // reverse sources in case dist is the preferred source for this package
+        if (!$this->preferSource && ($this->preferDist || 'dist' === $this->resolvePackageInstallPreference($package))) {
+            $sources = array_reverse($sources);
+        }
+
+        return $sources;
+    }
 }
 }

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

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

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

@@ -24,8 +24,9 @@ use Composer\Plugin\PluginEvents;
 use Composer\Plugin\PreFileDownloadEvent;
 use Composer\Plugin\PreFileDownloadEvent;
 use Composer\EventDispatcher\EventDispatcher;
 use Composer\EventDispatcher\EventDispatcher;
 use Composer\Util\Filesystem;
 use Composer\Util\Filesystem;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
 use Composer\Util\Url as UrlUtil;
 use Composer\Util\Url as UrlUtil;
+use Composer\Downloader\TransportException;
 
 
 /**
 /**
  * Base downloader for files
  * Base downloader for files
@@ -39,11 +40,13 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
 {
 {
     protected $io;
     protected $io;
     protected $config;
     protected $config;
-    protected $rfs;
+    protected $httpDownloader;
     protected $filesystem;
     protected $filesystem;
     protected $cache;
     protected $cache;
-    protected $outputProgress = true;
-    private $lastCacheWrites = array();
+    /**
+     * @private this is only public for php 5.3 support in closures
+     */
+    public $lastCacheWrites = array();
     private $eventDispatcher;
     private $eventDispatcher;
 
 
     /**
     /**
@@ -51,17 +54,17 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
      *
      *
      * @param IOInterface      $io              The IO instance
      * @param IOInterface      $io              The IO instance
      * @param Config           $config          The config
      * @param Config           $config          The config
+     * @param HttpDownloader   $httpDownloader  The remote filesystem
      * @param EventDispatcher  $eventDispatcher The event dispatcher
      * @param EventDispatcher  $eventDispatcher The event dispatcher
-     * @param Cache            $cache           Optional cache instance
-     * @param RemoteFilesystem $rfs             The remote filesystem
+     * @param Cache            $cache           Cache instance
      * @param Filesystem       $filesystem      The filesystem
      * @param Filesystem       $filesystem      The filesystem
      */
      */
-    public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, RemoteFilesystem $rfs = null, Filesystem $filesystem = null)
+    public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $filesystem = null)
     {
     {
         $this->io = $io;
         $this->io = $io;
         $this->config = $config;
         $this->config = $config;
         $this->eventDispatcher = $eventDispatcher;
         $this->eventDispatcher = $eventDispatcher;
-        $this->rfs = $rfs ?: Factory::createRemoteFilesystem($this->io, $config);
+        $this->httpDownloader = $httpDownloader;
         $this->filesystem = $filesystem ?: new Filesystem();
         $this->filesystem = $filesystem ?: new Filesystem();
         $this->cache = $cache;
         $this->cache = $cache;
 
 
@@ -87,121 +90,154 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
             throw new \InvalidArgumentException('The given package is missing url information');
             throw new \InvalidArgumentException('The given package is missing url information');
         }
         }
 
 
-        if ($output) {
-            $this->io->writeError("  - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>): ", false);
-        }
-
+        $retries = 3;
         $urls = $package->getDistUrls();
         $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);
         $this->filesystem->emptyDirectory($path);
-
         $fileName = $this->getFileName($package, $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();
             $checksum = $package->getDistSha1Checksum();
-            $cacheKey = $this->getCacheKey($package, $processedUrl);
+            $cacheKey = $url['cacheKey'];
 
 
             // use from cache if it is present and has a valid checksum or we have no checksum to check against
             // use from cache if it is present and has a valid checksum or we have no checksum to check against
-            if ($this->cache && (!$checksum || $checksum === $this->cache->sha1($cacheKey)) && $this->cache->copyTo($cacheKey, $fileName)) {
-                $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 {
             } 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;
             throw $e;
-        }
+        };
 
 
-        return $fileName;
+        return $download();
     }
     }
 
 
     /**
     /**
      * {@inheritDoc}
      * {@inheritDoc}
      */
      */
-    public function setOutputProgress($outputProgress)
+    public function install(PackageInterface $package, $path, $output = true)
     {
     {
-        $this->outputProgress = $outputProgress;
+        if ($output) {
+            $this->io->writeError("  - Installing <info>" . $package->getName() . "</info> (<comment>" . $package->getFullPrettyVersion() . "</comment>)");
+        }
 
 
-        return $this;
+        $this->filesystem->ensureDirectoryExists($path);
+        $this->filesystem->rename($this->getFileName($package, $path), $path . pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME));
     }
     }
 
 
-    protected function clearLastCacheWrite(PackageInterface $package)
+    /**
+     * TODO mark private in v3
+     * @protected This is public due to PHP 5.3
+     */
+    public function clearLastCacheWrite(PackageInterface $package)
     {
     {
         if ($this->cache && isset($this->lastCacheWrites[$package->getName()])) {
         if ($this->cache && isset($this->lastCacheWrites[$package->getName()])) {
             $this->cache->remove($this->lastCacheWrites[$package->getName()]);
             $this->cache->remove($this->lastCacheWrites[$package->getName()]);
@@ -222,7 +258,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
         $this->io->writeError("  - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>): ", false);
         $this->io->writeError("  - " . $actionName . " <info>" . $name . "</info> (<comment>" . $from . "</comment> => <comment>" . $to . "</comment>): ", false);
 
 
         $this->remove($initial, $path, false);
         $this->remove($initial, $path, false);
-        $this->download($target, $path, false);
+        $this->install($target, $path, false);
 
 
         $this->io->writeError('');
         $this->io->writeError('');
     }
     }
@@ -249,7 +285,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
      */
      */
     protected function getFileName(PackageInterface $package, $path)
     protected function getFileName(PackageInterface $package, $path)
     {
     {
-        return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
+        return $path.'_'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
     }
     }
 
 
     /**
     /**
@@ -291,15 +327,15 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
     public function getLocalChanges(PackageInterface $package, $targetDir)
     public function getLocalChanges(PackageInterface $package, $targetDir)
     {
     {
         $prevIO = $this->io;
         $prevIO = $this->io;
-        $prevProgress = $this->outputProgress;
 
 
         $this->io = new NullIO;
         $this->io = new NullIO;
         $this->io->loadConfiguration($this->config);
         $this->io->loadConfiguration($this->config);
-        $this->outputProgress = false;
         $e = null;
         $e = null;
 
 
         try {
         try {
-            $this->download($package, $targetDir.'_compare', false);
+            $res = $this->download($package, $targetDir.'_compare', false);
+            $this->httpDownloader->wait();
+            $res = $this->install($package, $targetDir.'_compare', false);
 
 
             $comparer = new Comparer();
             $comparer = new Comparer();
             $comparer->setSource($targetDir.'_compare');
             $comparer->setSource($targetDir.'_compare');
@@ -311,7 +347,6 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
         }
         }
 
 
         $this->io = $prevIO;
         $this->io = $prevIO;
-        $this->outputProgress = $prevProgress;
 
 
         if ($e) {
         if ($e) {
             throw $e;
             throw $e;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 30 - 34
src/Composer/Factory.php

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fichier diff supprimé car celui-ci est trop grand
+ 527 - 205
src/Composer/Repository/ComposerRepository.php


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -18,6 +18,8 @@ use Composer\Json\JsonFile;
 use Composer\Cache;
 use Composer\Cache;
 use Composer\IO\IOInterface;
 use Composer\IO\IOInterface;
 use Composer\Util\GitHub;
 use Composer\Util\GitHub;
+use Composer\Util\Http\Response;
+use Composer\Util\RemoteFilesystem;
 
 
 /**
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author Jordi Boggiano <j.boggiano@seld.be>
@@ -184,7 +186,7 @@ class GitHubDriver extends VcsDriver
         }
         }
 
 
         $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/contents/' . $file . '?ref='.urlencode($identifier);
         $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/contents/' . $file . '?ref='.urlencode($identifier);
-        $resource = JsonFile::parseJson($this->getContents($resource));
+        $resource = $this->getContents($resource)->decodeJson();
         if (empty($resource['content']) || $resource['encoding'] !== 'base64' || !($content = base64_decode($resource['content']))) {
         if (empty($resource['content']) || $resource['encoding'] !== 'base64' || !($content = base64_decode($resource['content']))) {
             throw new \RuntimeException('Could not retrieve ' . $file . ' for '.$identifier);
             throw new \RuntimeException('Could not retrieve ' . $file . ' for '.$identifier);
         }
         }
@@ -202,7 +204,7 @@ class GitHubDriver extends VcsDriver
         }
         }
 
 
         $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/commits/'.urlencode($identifier);
         $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/commits/'.urlencode($identifier);
-        $commit = JsonFile::parseJson($this->getContents($resource), $resource);
+        $commit = $this->getContents($resource)->decodeJson();
 
 
         return new \DateTime($commit['commit']['committer']['date']);
         return new \DateTime($commit['commit']['committer']['date']);
     }
     }
@@ -220,12 +222,13 @@ class GitHubDriver extends VcsDriver
             $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/tags?per_page=100';
             $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/tags?per_page=100';
 
 
             do {
             do {
-                $tagsData = JsonFile::parseJson($this->getContents($resource), $resource);
+                $response = $this->getContents($resource);
+                $tagsData = $response->decodeJson();
                 foreach ($tagsData as $tag) {
                 foreach ($tagsData as $tag) {
                     $this->tags[$tag['name']] = $tag['commit']['sha'];
                     $this->tags[$tag['name']] = $tag['commit']['sha'];
                 }
                 }
 
 
-                $resource = $this->getNextPage();
+                $resource = $this->getNextPage($response);
             } while ($resource);
             } while ($resource);
         }
         }
 
 
@@ -247,7 +250,8 @@ class GitHubDriver extends VcsDriver
             $branchBlacklist = array('gh-pages');
             $branchBlacklist = array('gh-pages');
 
 
             do {
             do {
-                $branchData = JsonFile::parseJson($this->getContents($resource), $resource);
+                $response = $this->getContents($resource);
+                $branchData = $response->decodeJson();
                 foreach ($branchData as $branch) {
                 foreach ($branchData as $branch) {
                     $name = substr($branch['ref'], 11);
                     $name = substr($branch['ref'], 11);
                     if (!in_array($name, $branchBlacklist)) {
                     if (!in_array($name, $branchBlacklist)) {
@@ -255,7 +259,7 @@ class GitHubDriver extends VcsDriver
                     }
                     }
                 }
                 }
 
 
-                $resource = $this->getNextPage();
+                $resource = $this->getNextPage($response);
             } while ($resource);
             } while ($resource);
         }
         }
 
 
@@ -315,7 +319,7 @@ class GitHubDriver extends VcsDriver
         try {
         try {
             return parent::getContents($url);
             return parent::getContents($url);
         } catch (TransportException $e) {
         } catch (TransportException $e) {
-            $gitHubUtil = new GitHub($this->io, $this->config, $this->process, $this->remoteFilesystem);
+            $gitHubUtil = new GitHub($this->io, $this->config, $this->process, $this->httpDownloader);
 
 
             switch ($e->getCode()) {
             switch ($e->getCode()) {
                 case 401:
                 case 401:
@@ -330,16 +334,18 @@ class GitHubDriver extends VcsDriver
                     }
                     }
 
 
                     if (!$this->io->isInteractive()) {
                     if (!$this->io->isInteractive()) {
-                        return $this->attemptCloneFallback();
+                        if ($this->attemptCloneFallback()) {
+                            return new Response(array('url' => 'dummy'), 200, array(), 'null');
+                        }
                     }
                     }
 
 
                     $scopesIssued = array();
                     $scopesIssued = array();
                     $scopesNeeded = array();
                     $scopesNeeded = array();
                     if ($headers = $e->getHeaders()) {
                     if ($headers = $e->getHeaders()) {
-                        if ($scopes = $this->remoteFilesystem->findHeaderValue($headers, 'X-OAuth-Scopes')) {
+                        if ($scopes = RemoteFilesystem::findHeaderValue($headers, 'X-OAuth-Scopes')) {
                             $scopesIssued = explode(' ', $scopes);
                             $scopesIssued = explode(' ', $scopes);
                         }
                         }
-                        if ($scopes = $this->remoteFilesystem->findHeaderValue($headers, 'X-Accepted-OAuth-Scopes')) {
+                        if ($scopes = RemoteFilesystem::findHeaderValue($headers, 'X-Accepted-OAuth-Scopes')) {
                             $scopesNeeded = explode(' ', $scopes);
                             $scopesNeeded = explode(' ', $scopes);
                         }
                         }
                     }
                     }
@@ -358,7 +364,9 @@ class GitHubDriver extends VcsDriver
                     }
                     }
 
 
                     if (!$this->io->isInteractive() && $fetchingRepoData) {
                     if (!$this->io->isInteractive() && $fetchingRepoData) {
-                        return $this->attemptCloneFallback();
+                        if ($this->attemptCloneFallback()) {
+                            return new Response(array('url' => 'dummy'), 200, array(), 'null');
+                        }
                     }
                     }
 
 
                     $rateLimited = $gitHubUtil->isRateLimited($e->getHeaders());
                     $rateLimited = $gitHubUtil->isRateLimited($e->getHeaders());
@@ -404,7 +412,7 @@ class GitHubDriver extends VcsDriver
 
 
         $repoDataUrl = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository;
         $repoDataUrl = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository;
 
 
-        $this->repoData = JsonFile::parseJson($this->getContents($repoDataUrl, true), $repoDataUrl);
+        $this->repoData = $this->getContents($repoDataUrl, true)->decodeJson();
         if (null === $this->repoData && null !== $this->gitDriver) {
         if (null === $this->repoData && null !== $this->gitDriver) {
             return;
             return;
         }
         }
@@ -434,7 +442,7 @@ class GitHubDriver extends VcsDriver
             // are not interactive) then we fallback to GitDriver.
             // are not interactive) then we fallback to GitDriver.
             $this->setupGitDriver($this->generateSshUrl());
             $this->setupGitDriver($this->generateSshUrl());
 
 
-            return;
+            return true;
         } catch (\RuntimeException $e) {
         } catch (\RuntimeException $e) {
             $this->gitDriver = null;
             $this->gitDriver = null;
 
 
@@ -449,23 +457,20 @@ class GitHubDriver extends VcsDriver
             array('url' => $url),
             array('url' => $url),
             $this->io,
             $this->io,
             $this->config,
             $this->config,
-            $this->process,
-            $this->remoteFilesystem
+            $this->httpDownloader,
+            $this->process
         );
         );
         $this->gitDriver->initialize();
         $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\IO\IOInterface;
 use Composer\Json\JsonFile;
 use Composer\Json\JsonFile;
 use Composer\Downloader\TransportException;
 use Composer\Downloader\TransportException;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\HttpDownloader;
 use Composer\Util\GitLab;
 use Composer\Util\GitLab;
+use Composer\Util\Http\Response;
 
 
 /**
 /**
  * Driver for GitLab API, use the Git driver for local checkouts.
  * Driver for GitLab API, use the Git driver for local checkouts.
@@ -110,14 +111,14 @@ class GitLabDriver extends VcsDriver
     }
     }
 
 
     /**
     /**
-     * Updates the RemoteFilesystem instance.
+     * Updates the HttpDownloader instance.
      * Mainly useful for tests.
      * Mainly useful for tests.
      *
      *
      * @internal
      * @internal
      */
      */
-    public function setRemoteFilesystem(RemoteFilesystem $remoteFilesystem)
+    public function setHttpDownloader(HttpDownloader $httpDownloader)
     {
     {
-        $this->remoteFilesystem = $remoteFilesystem;
+        $this->httpDownloader = $httpDownloader;
     }
     }
 
 
     /**
     /**
@@ -140,7 +141,7 @@ class GitLabDriver extends VcsDriver
         $resource = $this->getApiUrl().'/repository/files/'.$this->urlEncodeAll($file).'/raw?ref='.$identifier;
         $resource = $this->getApiUrl().'/repository/files/'.$this->urlEncodeAll($file).'/raw?ref='.$identifier;
 
 
         try {
         try {
-            $content = $this->getContents($resource);
+            $content = $this->getContents($resource)->getBody();
         } catch (TransportException $e) {
         } catch (TransportException $e) {
             if ($e->getCode() !== 404) {
             if ($e->getCode() !== 404) {
                 throw $e;
                 throw $e;
@@ -297,7 +298,8 @@ class GitLabDriver extends VcsDriver
 
 
         $references = array();
         $references = array();
         do {
         do {
-            $data = JsonFile::parseJson($this->getContents($resource), $resource);
+            $response = $this->getContents($resource);
+            $data = $response->decodeJson();
 
 
             foreach ($data as $datum) {
             foreach ($data as $datum) {
                 $references[$datum['name']] = $datum['commit']['id'];
                 $references[$datum['name']] = $datum['commit']['id'];
@@ -308,7 +310,7 @@ class GitLabDriver extends VcsDriver
             }
             }
 
 
             if (count($data) >= $perPage) {
             if (count($data) >= $perPage) {
-                $resource = $this->getNextPage();
+                $resource = $this->getNextPage($response);
             } else {
             } else {
                 $resource = false;
                 $resource = false;
             }
             }
@@ -321,7 +323,7 @@ class GitLabDriver extends VcsDriver
     {
     {
         // we need to fetch the default branch from the api
         // we need to fetch the default branch from the api
         $resource = $this->getApiUrl();
         $resource = $this->getApiUrl();
-        $this->project = JsonFile::parseJson($this->getContents($resource, true), $resource);
+        $this->project = $this->getContents($resource, true)->decodeJson();
         if (isset($this->project['visibility'])) {
         if (isset($this->project['visibility'])) {
             $this->isPrivate = $this->project['visibility'] !== 'public';
             $this->isPrivate = $this->project['visibility'] !== 'public';
         } else {
         } else {
@@ -344,7 +346,7 @@ class GitLabDriver extends VcsDriver
             // are not interactive) then we fallback to GitDriver.
             // are not interactive) then we fallback to GitDriver.
             $this->setupGitDriver($url);
             $this->setupGitDriver($url);
 
 
-            return;
+            return true;
         } catch (\RuntimeException $e) {
         } catch (\RuntimeException $e) {
             $this->gitDriver = null;
             $this->gitDriver = null;
 
 
@@ -374,8 +376,8 @@ class GitLabDriver extends VcsDriver
             array('url' => $url),
             array('url' => $url),
             $this->io,
             $this->io,
             $this->config,
             $this->config,
-            $this->process,
-            $this->remoteFilesystem
+            $this->httpDownloader,
+            $this->process
         );
         );
         $this->gitDriver->initialize();
         $this->gitDriver->initialize();
     }
     }
@@ -386,10 +388,10 @@ class GitLabDriver extends VcsDriver
     protected function getContents($url, $fetchingRepoData = false)
     protected function getContents($url, $fetchingRepoData = false)
     {
     {
         try {
         try {
-            $res = parent::getContents($url);
+            $response = parent::getContents($url);
 
 
             if ($fetchingRepoData) {
             if ($fetchingRepoData) {
-                $json = JsonFile::parseJson($res, $url);
+                $json = $response->decodeJson();
 
 
                 // force auth as the unauthenticated version of the API is broken
                 // force auth as the unauthenticated version of the API is broken
                 if (!isset($json['default_branch'])) {
                 if (!isset($json['default_branch'])) {
@@ -401,9 +403,9 @@ class GitLabDriver extends VcsDriver
                 }
                 }
             }
             }
 
 
-            return $res;
+            return $response;
         } catch (TransportException $e) {
         } catch (TransportException $e) {
-            $gitLabUtil = new GitLab($this->io, $this->config, $this->process, $this->remoteFilesystem);
+            $gitLabUtil = new GitLab($this->io, $this->config, $this->process, $this->httpDownloader);
 
 
             switch ($e->getCode()) {
             switch ($e->getCode()) {
                 case 401:
                 case 401:
@@ -418,7 +420,9 @@ class GitLabDriver extends VcsDriver
                     }
                     }
 
 
                     if (!$this->io->isInteractive()) {
                     if (!$this->io->isInteractive()) {
-                        return $this->attemptCloneFallback();
+                        if ($this->attemptCloneFallback()) {
+                            return new Response(array('url' => 'dummy'), 200, array(), 'null');
+                        }
                     }
                     }
                     $this->io->writeError('<warning>Failed to download ' . $this->namespace . '/' . $this->repository . ':' . $e->getMessage() . '</warning>');
                     $this->io->writeError('<warning>Failed to download ' . $this->namespace . '/' . $this->repository . ':' . $e->getMessage() . '</warning>');
                     $gitLabUtil->authorizeOAuthInteractively($this->scheme, $this->originUrl, 'Your credentials are required to fetch private repository metadata (<info>'.$this->url.'</info>)');
                     $gitLabUtil->authorizeOAuthInteractively($this->scheme, $this->originUrl, 'Your credentials are required to fetch private repository metadata (<info>'.$this->url.'</info>)');
@@ -431,7 +435,9 @@ class GitLabDriver extends VcsDriver
                     }
                     }
 
 
                     if (!$this->io->isInteractive() && $fetchingRepoData) {
                     if (!$this->io->isInteractive() && $fetchingRepoData) {
-                        return $this->attemptCloneFallback();
+                        if ($this->attemptCloneFallback()) {
+                            return new Response(array('url' => 'dummy'), 200, array(), 'null');
+                        }
                     }
                     }
 
 
                     throw $e;
                     throw $e;
@@ -471,17 +477,14 @@ class GitLabDriver extends VcsDriver
         return true;
         return true;
     }
     }
 
 
-    private function getNextPage()
+    protected function getNextPage(Response $response)
     {
     {
-        $headers = $this->remoteFilesystem->getLastHeaders();
-        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),
             array('url' => $url),
             $this->io,
             $this->io,
             $this->config,
             $this->config,
-            $this->process,
-            $this->remoteFilesystem
+            $this->httpDownloader,
+            $this->process
         );
         );
         $this->fallbackDriver->initialize();
         $this->fallbackDriver->initialize();
     }
     }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -50,7 +50,7 @@ class DownloadManagerTest extends TestCase
 
 
         $this->setExpectedException('InvalidArgumentException');
         $this->setExpectedException('InvalidArgumentException');
 
 
-        $manager->getDownloaderForInstalledPackage($package);
+        $manager->getDownloaderForPackage($package);
     }
     }
 
 
     public function testGetDownloaderForCorrectlyInstalledDistPackage()
     public function testGetDownloaderForCorrectlyInstalledDistPackage()
@@ -82,7 +82,7 @@ class DownloadManagerTest extends TestCase
             ->with('pear')
             ->with('pear')
             ->will($this->returnValue($downloader));
             ->will($this->returnValue($downloader));
 
 
-        $this->assertSame($downloader, $manager->getDownloaderForInstalledPackage($package));
+        $this->assertSame($downloader, $manager->getDownloaderForPackage($package));
     }
     }
 
 
     public function testGetDownloaderForIncorrectlyInstalledDistPackage()
     public function testGetDownloaderForIncorrectlyInstalledDistPackage()
@@ -116,7 +116,7 @@ class DownloadManagerTest extends TestCase
 
 
         $this->setExpectedException('LogicException');
         $this->setExpectedException('LogicException');
 
 
-        $manager->getDownloaderForInstalledPackage($package);
+        $manager->getDownloaderForPackage($package);
     }
     }
 
 
     public function testGetDownloaderForCorrectlyInstalledSourcePackage()
     public function testGetDownloaderForCorrectlyInstalledSourcePackage()
@@ -148,7 +148,7 @@ class DownloadManagerTest extends TestCase
             ->with('git')
             ->with('git')
             ->will($this->returnValue($downloader));
             ->will($this->returnValue($downloader));
 
 
-        $this->assertSame($downloader, $manager->getDownloaderForInstalledPackage($package));
+        $this->assertSame($downloader, $manager->getDownloaderForPackage($package));
     }
     }
 
 
     public function testGetDownloaderForIncorrectlyInstalledSourcePackage()
     public function testGetDownloaderForIncorrectlyInstalledSourcePackage()
@@ -182,7 +182,7 @@ class DownloadManagerTest extends TestCase
 
 
         $this->setExpectedException('LogicException');
         $this->setExpectedException('LogicException');
 
 
-        $manager->getDownloaderForInstalledPackage($package);
+        $manager->getDownloaderForPackage($package);
     }
     }
 
 
     public function testGetDownloaderForMetapackage()
     public function testGetDownloaderForMetapackage()
@@ -195,7 +195,7 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = new DownloadManager($this->io, false, $this->filesystem);
         $manager = new DownloadManager($this->io, false, $this->filesystem);
 
 
-        $this->assertNull($manager->getDownloaderForInstalledPackage($package));
+        $this->assertNull($manager->getDownloaderForPackage($package));
     }
     }
 
 
     public function testFullPackageDownload()
     public function testFullPackageDownload()
@@ -223,11 +223,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
             ->getMock();
         $manager
         $manager
             ->expects($this->once())
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->with($package)
             ->will($this->returnValue($downloader));
             ->will($this->returnValue($downloader));
 
 
@@ -274,16 +274,16 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
             ->getMock();
         $manager
         $manager
             ->expects($this->at(0))
             ->expects($this->at(0))
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->with($package)
             ->will($this->returnValue($downloaderFail));
             ->will($this->returnValue($downloaderFail));
         $manager
         $manager
             ->expects($this->at(1))
             ->expects($this->at(1))
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->with($package)
             ->will($this->returnValue($downloaderSuccess));
             ->will($this->returnValue($downloaderSuccess));
 
 
@@ -333,11 +333,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
             ->getMock();
         $manager
         $manager
             ->expects($this->once())
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->with($package)
             ->will($this->returnValue($downloader));
             ->will($this->returnValue($downloader));
 
 
@@ -369,11 +369,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
             ->getMock();
         $manager
         $manager
             ->expects($this->once())
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->with($package)
             ->will($this->returnValue($downloader));
             ->will($this->returnValue($downloader));
 
 
@@ -399,11 +399,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
           ->setConstructorArgs(array($this->io, false, $this->filesystem))
           ->setConstructorArgs(array($this->io, false, $this->filesystem))
-          ->setMethods(array('getDownloaderForInstalledPackage'))
+          ->setMethods(array('getDownloaderForPackage'))
           ->getMock();
           ->getMock();
         $manager
         $manager
           ->expects($this->once())
           ->expects($this->once())
-          ->method('getDownloaderForInstalledPackage')
+          ->method('getDownloaderForPackage')
           ->with($package)
           ->with($package)
           ->will($this->returnValue(null)); // There is no downloader for Metapackages.
           ->will($this->returnValue(null)); // There is no downloader for Metapackages.
 
 
@@ -435,11 +435,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
             ->getMock();
         $manager
         $manager
             ->expects($this->once())
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->with($package)
             ->will($this->returnValue($downloader));
             ->will($this->returnValue($downloader));
 
 
@@ -472,11 +472,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
             ->getMock();
         $manager
         $manager
             ->expects($this->once())
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->with($package)
             ->will($this->returnValue($downloader));
             ->will($this->returnValue($downloader));
 
 
@@ -509,11 +509,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
             ->getMock();
         $manager
         $manager
             ->expects($this->once())
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->with($package)
             ->will($this->returnValue($downloader));
             ->will($this->returnValue($downloader));
 
 
@@ -550,33 +550,30 @@ class DownloadManagerTest extends TestCase
         $initial
         $initial
             ->expects($this->once())
             ->expects($this->once())
             ->method('getDistType')
             ->method('getDistType')
-            ->will($this->returnValue('pear'));
+            ->will($this->returnValue('zip'));
 
 
         $target = $this->createPackageMock();
         $target = $this->createPackageMock();
         $target
         $target
             ->expects($this->once())
             ->expects($this->once())
-            ->method('getDistType')
-            ->will($this->returnValue('pear'));
+            ->method('getInstallationSource')
+            ->will($this->returnValue('dist'));
         $target
         $target
             ->expects($this->once())
             ->expects($this->once())
-            ->method('setInstallationSource')
-            ->with('dist');
+            ->method('getDistType')
+            ->will($this->returnValue('zip'));
 
 
-        $pearDownloader = $this->createDownloaderMock();
-        $pearDownloader
+        $zipDownloader = $this->createDownloaderMock();
+        $zipDownloader
             ->expects($this->once())
             ->expects($this->once())
             ->method('update')
             ->method('update')
             ->with($initial, $target, 'vendor/bundles/FOS/UserBundle');
             ->with($initial, $target, 'vendor/bundles/FOS/UserBundle');
+        $zipDownloader
+            ->expects($this->any())
+            ->method('getInstallationSource')
+            ->will($this->returnValue('dist'));
 
 
-        $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
-            ->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');
         $manager->update($initial, $target, 'vendor/bundles/FOS/UserBundle');
     }
     }
@@ -591,113 +588,89 @@ class DownloadManagerTest extends TestCase
         $initial
         $initial
             ->expects($this->once())
             ->expects($this->once())
             ->method('getDistType')
             ->method('getDistType')
-            ->will($this->returnValue('pear'));
+            ->will($this->returnValue('xz'));
 
 
         $target = $this->createPackageMock();
         $target = $this->createPackageMock();
         $target
         $target
-            ->expects($this->once())
+            ->expects($this->any())
+            ->method('getInstallationSource')
+            ->will($this->returnValue('dist'));
+        $target
+            ->expects($this->any())
             ->method('getDistType')
             ->method('getDistType')
-            ->will($this->returnValue('composer'));
+            ->will($this->returnValue('zip'));
 
 
-        $pearDownloader = $this->createDownloaderMock();
-        $pearDownloader
+        $xzDownloader = $this->createDownloaderMock();
+        $xzDownloader
             ->expects($this->once())
             ->expects($this->once())
             ->method('remove')
             ->method('remove')
             ->with($initial, 'vendor/bundles/FOS/UserBundle');
             ->with($initial, 'vendor/bundles/FOS/UserBundle');
+        $xzDownloader
+            ->expects($this->any())
+            ->method('getInstallationSource')
+            ->will($this->returnValue('dist'));
 
 
-        $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
-            ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage', 'download'))
-            ->getMock();
-        $manager
+        $zipDownloader = $this->createDownloaderMock();
+        $zipDownloader
             ->expects($this->once())
             ->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');
         $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()
     public function testUpdateMetapackage()
@@ -707,11 +680,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
           ->setConstructorArgs(array($this->io, false, $this->filesystem))
           ->setConstructorArgs(array($this->io, false, $this->filesystem))
-          ->setMethods(array('getDownloaderForInstalledPackage'))
+          ->setMethods(array('getDownloaderForPackage'))
           ->getMock();
           ->getMock();
         $manager
         $manager
-          ->expects($this->once())
-          ->method('getDownloaderForInstalledPackage')
+          ->expects($this->exactly(2))
+          ->method('getDownloaderForPackage')
           ->with($initial)
           ->with($initial)
           ->will($this->returnValue(null)); // There is no downloader for metapackages.
           ->will($this->returnValue(null)); // There is no downloader for metapackages.
 
 
@@ -730,11 +703,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
             ->getMock();
         $manager
         $manager
             ->expects($this->once())
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->with($package)
             ->will($this->returnValue($pearDownloader));
             ->will($this->returnValue($pearDownloader));
 
 
@@ -747,11 +720,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
           ->setConstructorArgs(array($this->io, false, $this->filesystem))
           ->setConstructorArgs(array($this->io, false, $this->filesystem))
-          ->setMethods(array('getDownloaderForInstalledPackage'))
+          ->setMethods(array('getDownloaderForPackage'))
           ->getMock();
           ->getMock();
         $manager
         $manager
           ->expects($this->once())
           ->expects($this->once())
-          ->method('getDownloaderForInstalledPackage')
+          ->method('getDownloaderForPackage')
           ->with($package)
           ->with($package)
           ->will($this->returnValue(null)); // There is no downloader for metapackages.
           ->will($this->returnValue(null)); // There is no downloader for metapackages.
 
 
@@ -790,11 +763,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
             ->getMock();
         $manager
         $manager
             ->expects($this->once())
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->with($package)
             ->will($this->returnValue($downloader));
             ->will($this->returnValue($downloader));
 
 
@@ -833,11 +806,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
             ->getMock();
         $manager
         $manager
             ->expects($this->once())
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->with($package)
             ->will($this->returnValue($downloader));
             ->will($this->returnValue($downloader));
 
 
@@ -879,11 +852,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
             ->getMock();
         $manager
         $manager
             ->expects($this->once())
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->with($package)
             ->will($this->returnValue($downloader));
             ->will($this->returnValue($downloader));
         $manager->setPreferences(array('foo/*' => 'source'));
         $manager->setPreferences(array('foo/*' => 'source'));
@@ -926,11 +899,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
             ->getMock();
         $manager
         $manager
             ->expects($this->once())
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->with($package)
             ->will($this->returnValue($downloader));
             ->will($this->returnValue($downloader));
         $manager->setPreferences(array('foo/*' => 'source'));
         $manager->setPreferences(array('foo/*' => 'source'));
@@ -973,11 +946,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
             ->getMock();
         $manager
         $manager
             ->expects($this->once())
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->with($package)
             ->will($this->returnValue($downloader));
             ->will($this->returnValue($downloader));
         $manager->setPreferences(array('foo/*' => 'auto'));
         $manager->setPreferences(array('foo/*' => 'auto'));
@@ -1020,11 +993,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
             ->getMock();
         $manager
         $manager
             ->expects($this->once())
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->with($package)
             ->will($this->returnValue($downloader));
             ->will($this->returnValue($downloader));
         $manager->setPreferences(array('foo/*' => 'auto'));
         $manager->setPreferences(array('foo/*' => 'auto'));
@@ -1063,11 +1036,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
             ->getMock();
         $manager
         $manager
             ->expects($this->once())
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->with($package)
             ->will($this->returnValue($downloader));
             ->will($this->returnValue($downloader));
         $manager->setPreferences(array('foo/*' => 'source'));
         $manager->setPreferences(array('foo/*' => 'source'));
@@ -1106,11 +1079,11 @@ class DownloadManagerTest extends TestCase
 
 
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
         $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
             ->setConstructorArgs(array($this->io, false, $this->filesystem))
-            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->setMethods(array('getDownloaderForPackage'))
             ->getMock();
             ->getMock();
         $manager
         $manager
             ->expects($this->once())
             ->expects($this->once())
-            ->method('getDownloaderForInstalledPackage')
+            ->method('getDownloaderForPackage')
             ->with($package)
             ->with($package)
             ->will($this->returnValue($downloader));
             ->will($this->returnValue($downloader));
         $manager->setPreferences(array('foo/*' => 'dist'));
         $manager->setPreferences(array('foo/*' => 'dist'));

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -18,7 +18,7 @@ use Composer\Repository\RepositoryInterface;
 use Composer\Test\Mock\FactoryMock;
 use Composer\Test\Mock\FactoryMock;
 use Composer\Test\TestCase;
 use Composer\Test\TestCase;
 use Composer\Package\Loader\ArrayLoader;
 use Composer\Package\Loader\ArrayLoader;
-use Composer\Semver\VersionParser;
+use Composer\Package\Version\VersionParser;
 
 
 class ComposerRepositoryTest extends TestCase
 class ComposerRepositoryTest extends TestCase
 {
 {
@@ -32,11 +32,13 @@ class ComposerRepositoryTest extends TestCase
         );
         );
 
 
         $repository = $this->getMockBuilder('Composer\Repository\ComposerRepository')
         $repository = $this->getMockBuilder('Composer\Repository\ComposerRepository')
-            ->setMethods(array('loadRootServerFile', 'createPackage'))
+            ->setMethods(array('loadRootServerFile', 'createPackages'))
             ->setConstructorArgs(array(
             ->setConstructorArgs(array(
                 $repoConfig,
                 $repoConfig,
                 new NullIO,
                 new NullIO,
                 FactoryMock::createConfig(),
                 FactoryMock::createConfig(),
+                $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(),
+                $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock()
             ))
             ))
             ->getMock();
             ->getMock();
 
 
@@ -45,16 +47,17 @@ class ComposerRepositoryTest extends TestCase
             ->method('loadRootServerFile')
             ->method('loadRootServerFile')
             ->will($this->returnValue($repoPackages));
             ->will($this->returnValue($repoPackages));
 
 
+        $stubs = array();
         foreach ($expected as $at => $arg) {
         foreach ($expected as $at => $arg) {
-            $stubPackage = $this->getPackage('stub/stub', '1.0.0');
-
-            $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
         // Triggers initialization
         $packages = $repository->getPackages();
         $packages = $repository->getPackages();
 
 
@@ -143,19 +146,12 @@ class ComposerRepositoryTest extends TestCase
             )));
             )));
 
 
         $versionParser = new VersionParser();
         $versionParser = new VersionParser();
-        $repo->setRootAliases(array(
-            '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());
         $this->assertSame($packages['2'], $packages['2-alias']->getAliasOf());
     }
     }
 
 
@@ -179,21 +175,29 @@ class ComposerRepositoryTest extends TestCase
             ),
             ),
         );
         );
 
 
-        $rfs = $this->getMockBuilder('Composer\Util\RemoteFilesystem')
+        $httpDownloader = $this->getMockBuilder('Composer\Util\HttpDownloader')
+            ->disableOriginalConstructor()
+            ->getMock();
+        $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
             ->disableOriginalConstructor()
             ->disableOriginalConstructor()
             ->getMock();
             ->getMock();
 
 
-        $rfs->expects($this->at(0))
-            ->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(
         $this->assertSame(
             array(array('name' => 'foo', 'description' => null)),
             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\Package\Loader\ArrayLoader;
 use Composer\Repository\PathRepository;
 use Composer\Repository\PathRepository;
-use Composer\Semver\VersionParser;
 use Composer\Test\TestCase;
 use Composer\Test\TestCase;
+use Composer\Package\Version\VersionParser;
 
 
 class PathRepositoryTest extends TestCase
 class PathRepositoryTest extends TestCase
 {
 {

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

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

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff