Browse Source

Merge remote-tracking branch 'upstream/master' into repro-4795

* upstream/master: (98 commits)
  Fallback to zlib extension to unpack gzip on non Windows systems
  Zip extension does not provide zlib support
  Unified all Windows tests throughout the code.
  Added Platform utility and unit test for it.
  Remove warnings for non-writable dirs, refs #3588
  [doc] add -H flag to sudo commands
  use full json content to determine reference, closes #4859
  typos
  Make sure COMPOSER_AUTH is also loaded in Config, refs #4546
  Use proper defaults for IO authentications
  Add verbosity input support to IOInterface
  Update SolverTest.php
  Update broken-deps-do-not-replace.test
  Update SolverProblemsException.php
  Cleaned up check+conversion that was no longer required.
  Cleaner notation for expected exceptions in fixtures.
  Introduced more generic, less invasive way to test for exceptions in fixtures, more in line with how phpunit works.
  Included unit test for circular root dependencies.
  Expanded InstallerTest to support expecting Exceptions by supplying "EXCEPTION" as "--EXPECT--"
  Clarified error message and added braces.
  ...
Rob Bast 9 years ago
parent
commit
be5719eb53
100 changed files with 1654 additions and 556 deletions
  1. 16 16
      .php_cs
  2. 1 1
      CHANGELOG.md
  3. 2 1
      composer.json
  4. 1 1
      composer.lock
  5. 1 1
      doc/00-intro.md
  6. 14 1
      doc/03-cli.md
  7. 5 0
      doc/05-repositories.md
  8. 9 3
      doc/06-config.md
  9. 1 1
      doc/articles/custom-installers.md
  10. 48 3
      doc/articles/plugins.md
  11. 4 4
      doc/articles/troubleshooting.md
  12. 4 0
      res/composer-schema.json
  13. 2 1
      src/Composer/Autoload/ClassMapGenerator.php
  14. 6 13
      src/Composer/Cache.php
  15. 25 10
      src/Composer/Command/ConfigCommand.php
  16. 4 4
      src/Composer/Command/CreateProjectCommand.php
  17. 1 1
      src/Composer/Command/DependsCommand.php
  18. 33 0
      src/Composer/Command/DiagnoseCommand.php
  19. 2 1
      src/Composer/Command/HomeCommand.php
  20. 2 1
      src/Composer/Command/RemoveCommand.php
  21. 3 2
      src/Composer/Command/RequireCommand.php
  22. 121 11
      src/Composer/Command/SelfUpdateCommand.php
  23. 24 23
      src/Composer/Command/ShowCommand.php
  24. 4 2
      src/Composer/Config.php
  25. 2 1
      src/Composer/Config/JsonConfigSource.php
  26. 15 14
      src/Composer/Console/Application.php
  27. 46 1
      src/Composer/DependencyResolver/SolverProblemsException.php
  28. 2 3
      src/Composer/Downloader/ArchiveDownloader.php
  29. 1 3
      src/Composer/Downloader/FileDownloader.php
  30. 3 2
      src/Composer/Downloader/GitDownloader.php
  31. 21 8
      src/Composer/Downloader/GzipDownloader.php
  32. 3 2
      src/Composer/Downloader/RarDownloader.php
  33. 3 2
      src/Composer/Downloader/ZipDownloader.php
  34. 1 3
      src/Composer/EventDispatcher/EventDispatcher.php
  35. 37 24
      src/Composer/Factory.php
  36. 12 14
      src/Composer/IO/BaseIO.php
  37. 1 1
      src/Composer/IO/BufferIO.php
  38. 32 17
      src/Composer/IO/ConsoleIO.php
  39. 24 14
      src/Composer/IO/IOInterface.php
  40. 4 4
      src/Composer/IO/NullIO.php
  41. 4 8
      src/Composer/Installer.php
  42. 7 5
      src/Composer/Installer/LibraryInstaller.php
  43. 3 4
      src/Composer/Installer/PearInstaller.php
  44. 1 1
      src/Composer/Package/AliasPackage.php
  45. 5 0
      src/Composer/Package/Loader/RootPackageLoader.php
  46. 23 0
      src/Composer/Plugin/Capability/Capability.php
  47. 43 0
      src/Composer/Plugin/Capable.php
  48. 2 2
      src/Composer/Plugin/PluginInterface.php
  49. 60 4
      src/Composer/Plugin/PluginManager.php
  50. 3 7
      src/Composer/Repository/ArtifactRepository.php
  51. 1 0
      src/Composer/Repository/ComposerRepository.php
  52. 9 6
      src/Composer/Repository/PathRepository.php
  53. 1 3
      src/Composer/Repository/PearRepository.php
  54. 1 0
      src/Composer/Repository/PlatformRepository.php
  55. 0 1
      src/Composer/Repository/RepositoryManager.php
  56. 1 3
      src/Composer/Repository/Vcs/GitBitbucketDriver.php
  57. 1 3
      src/Composer/Repository/Vcs/GitHubDriver.php
  58. 1 3
      src/Composer/Repository/Vcs/GitLabDriver.php
  59. 1 3
      src/Composer/Repository/Vcs/HgBitbucketDriver.php
  60. 0 1
      src/Composer/Util/ConfigValidator.php
  61. 3 2
      src/Composer/Util/ErrorHandler.php
  62. 8 8
      src/Composer/Util/Filesystem.php
  63. 36 0
      src/Composer/Util/Keys.php
  64. 1 4
      src/Composer/Util/Perforce.php
  65. 28 0
      src/Composer/Util/Platform.php
  66. 1 1
      src/Composer/Util/ProcessExecutor.php
  67. 345 108
      src/Composer/Util/RemoteFilesystem.php
  68. 77 0
      src/Composer/Util/Silencer.php
  69. 210 0
      src/Composer/Util/TlsHelper.php
  70. 12 7
      tests/Composer/Test/AllFunctionalTest.php
  71. 9 0
      tests/Composer/Test/ApplicationTest.php
  72. 2 2
      tests/Composer/Test/Autoload/AutoloadGeneratorTest.php
  73. 5 10
      tests/Composer/Test/Autoload/ClassMapGeneratorTest.php
  74. 3 3
      tests/Composer/Test/CacheTest.php
  75. 15 0
      tests/Composer/Test/Config/Fixtures/config/config-with-exampletld-repository-and-options.json
  76. 21 3
      tests/Composer/Test/Config/JsonConfigSourceTest.php
  77. 10 0
      tests/Composer/Test/ConfigTest.php
  78. 1 2
      tests/Composer/Test/DefaultConfigTest.php
  79. 1 1
      tests/Composer/Test/DependencyResolver/SolverTest.php
  80. 6 13
      tests/Composer/Test/Downloader/FileDownloaderTest.php
  81. 6 4
      tests/Composer/Test/Downloader/GitDownloaderTest.php
  82. 5 7
      tests/Composer/Test/Downloader/HgDownloaderTest.php
  83. 3 2
      tests/Composer/Test/Downloader/PearPackageExtractorTest.php
  84. 3 2
      tests/Composer/Test/Downloader/PerforceDownloaderTest.php
  85. 6 4
      tests/Composer/Test/Downloader/XzDownloaderTest.php
  86. 8 3
      tests/Composer/Test/Downloader/ZipDownloaderTest.php
  87. 15 42
      tests/Composer/Test/EventDispatcher/EventDispatcherTest.php
  88. 4 4
      tests/Composer/Test/Fixtures/installer/abandoned-listed.test
  89. 4 4
      tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test
  90. 16 0
      tests/Composer/Test/Fixtures/installer/install-self-from-root.test
  91. 4 4
      tests/Composer/Test/Fixtures/installer/suggest-installed.test
  92. 4 4
      tests/Composer/Test/Fixtures/installer/suggest-prod.test
  93. 4 4
      tests/Composer/Test/Fixtures/installer/suggest-replaced.test
  94. 4 4
      tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test
  95. 20 7
      tests/Composer/Test/IO/ConsoleIOTest.php
  96. 5 4
      tests/Composer/Test/Installer/LibraryInstallerTest.php
  97. 32 16
      tests/Composer/Test/InstallerTest.php
  98. 3 2
      tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php
  99. 3 3
      tests/Composer/Test/Package/Archiver/ArchiverTest.php
  100. 4 4
      tests/Composer/Test/Package/Archiver/PharArchiverTest.php

+ 16 - 16
.php_cs

@@ -10,7 +10,7 @@ For the full copyright and license information, please view the LICENSE
 file that was distributed with this source code.
 EOF;
 
-$finder = Symfony\CS\Finder\DefaultFinder::create()
+$finder = Symfony\CS\Finder::create()
     ->files()
     ->name('*.php')
     ->exclude('Fixtures')
@@ -18,23 +18,27 @@ $finder = Symfony\CS\Finder\DefaultFinder::create()
     ->in(__DIR__.'/tests')
 ;
 
-return Symfony\CS\Config\Config::create()
+return Symfony\CS\Config::create()
     ->setUsingCache(true)
     ->setRiskyAllowed(true)
     ->setRules(array(
         '@PSR2' => true,
-        'duplicate_semicolon' => true,
-        'extra_empty_lines' => true,
+        'binary_operator_spaces' => true,
+        'blank_line_before_return' => true,
         'header_comment' => array('header' => $header),
         'include' => true,
         'long_array_syntax' => true,
         'method_separation' => true,
-        'multiline_array_trailing_comma' => true,
-        'namespace_no_leading_whitespace' => true,
         'no_blank_lines_after_class_opening' => true,
-        'no_empty_lines_after_phpdocs' => true,
-        'object_operator' => true,
-        'operators_spaces' => true,
+        'no_blank_lines_after_phpdoc' => true,
+        'no_blank_lines_between_uses' => true,
+        'no_duplicate_semicolons' => true,
+        'no_extra_consecutive_blank_lines' => true,
+        'no_leading_import_slash' => true,
+        'no_leading_namespace_whitespace' => true,
+        'no_trailing_comma_in_singleline_array' => true,
+        'no_unused_imports' => true,
+        'object_operator_without_whitespace' => true,
         'phpdoc_align' => true,
         'phpdoc_indent' => true,
         'phpdoc_no_access' => true,
@@ -44,15 +48,11 @@ return Symfony\CS\Config\Config::create()
         'phpdoc_trim' => true,
         'phpdoc_type_to_var' => true,
         'psr0' => true,
-        'return' => true,
-        'remove_leading_slash_use' => true,
-        'remove_lines_between_uses' => true,
-        'single_array_no_trailing_comma' => true,
         'single_blank_line_before_namespace' => true,
         'spaces_cast' => true,
-        'standardize_not_equal' => true,
-        'ternary_spaces' => true,
-        'unused_use' => true,
+        'standardize_not_equals' => true,
+        'ternary_operator_spaces' => true,
+        'trailing_comma_in_multiline_array' => true,
         'whitespacy_lines' => true,
     ))
     ->finder($finder)

+ 1 - 1
CHANGELOG.md

@@ -12,7 +12,7 @@
   * Added --strict to the `validate` command to treat any warning as an error that then returns a non-zero exit code
   * Added a dependency on composer/semver, which is the externalized lib for all the version constraints parsing and handling
   * Added support for classmap autoloading to load plugin classes and script handlers
-  * Added `bin-compat` config option that if set to `full` will create .bat proxy for binaries even if Compoesr runs in a linux VM
+  * Added `bin-compat` config option that if set to `full` will create .bat proxy for binaries even if Composer runs in a linux VM
   * Added SPDX 2.0 support, and externalized that in a composer/spdx-licenses lib
   * Added warnings when the classmap autoloader finds duplicate classes
   * Added --file to the `archive` command to choose the filename

+ 2 - 1
composer.json

@@ -45,7 +45,8 @@
         }
     },
     "suggest": {
-        "ext-zip": "Enabling the zip extension allows you to unzip archives, and allows gzip compression of all internet traffic",
+        "ext-zip": "Enabling the zip extension allows you to unzip archives",
+        "ext-zlib": "Allow gzip compression of HTTP requests",
         "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages"
     },
     "autoload": {

+ 1 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "fdf4b487fa59607376721ebec4ff4783",
+    "hash": "31b3c13c89f8d6c810637ca1fe8fc6ae",
     "content-hash": "454148e20b837d9755dee7862f9c7a5d",
     "packages": [
         {

+ 1 - 1
doc/00-intro.md

@@ -109,7 +109,7 @@ mv composer.phar /usr/local/bin/composer
 A quick copy-paste version including sudo:
 
 ```sh
-curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer
+curl -sS https://getcomposer.org/installer | sudo -H php -- --install-dir=/usr/local/bin --filename=composer
 ```
 
 > **Note:** On some versions of OSX the `/usr` directory does not exist by

+ 14 - 1
doc/03-cli.md

@@ -413,7 +413,7 @@ If you have installed Composer for your entire system (see [global installation]
 you may have to run the command with `root` privileges
 
 ```sh
-sudo composer self-update
+sudo -H composer self-update
 ```
 
 ### Options
@@ -466,6 +466,12 @@ changes to the repositories section by using it the following way:
 php composer.phar config repositories.foo vcs https://github.com/foo/bar
 ```
 
+If your repository requires more configuration options, you can instead pass its JSON representation :
+
+```sh
+php composer.phar config repositories.foo '{"type": "vcs", "url": "http://svn.example.org/my-project/", "trunk-path": "master"}'
+```
+
 ## create-project
 
 You can use Composer to create new projects from an existing package. This is
@@ -711,6 +717,13 @@ commands) to finish executing. The default value is 300 seconds (5 minutes).
 By setting this environmental value, you can set a path to a certificate bundle
 file to be used during SSL/TLS peer verification.
 
+### COMPOSER_AUTH
+
+The `COMPOSER_AUTH` var allows you to set up authentication as an environment variable.
+The contents of the variable should be a JSON formatted object containing http-basic,
+github-oauth, ... objects as needed, and following the
+[spec from the config](06-config.md#gitlab-oauth).
+
 ### COMPOSER_DISCARD_CHANGES
 
 This env var controls the [`discard-changes`](06-config.md#discard-changes) config option.

+ 5 - 0
doc/05-repositories.md

@@ -639,6 +639,11 @@ file, you can use the following configuration:
 }
 ```
 
+If the package is a local VCS repository, the version may be inferred by
+the branch or tag that is currently checked out. Otherwise, the version should
+be explicitly defined in the package's `composer.json` file. If the version
+cannot be resolved by these means, it is assumed to be `dev-master`.
+
 The local package will be symlinked if possible, in which case the output in
 the console will read `Symlinked from ../../packages/my-package`. If symlinking
 is _not_ possible the package will be copied. In that case, the console will

+ 9 - 3
doc/06-config.md

@@ -55,9 +55,15 @@ php_openssl extension in php.ini.
 
 ## cafile
 
-A way to set the path to the openssl CA file. In PHP 5.6+ you should rather
-set this via openssl.cafile in php.ini, although PHP 5.6+ should be able to
-detect your system CA file automatically.
+Location of Certificate Authority file on local filesystem. In PHP 5.6+ you
+should rather set this via openssl.cafile in php.ini, although PHP 5.6+ should
+be able to detect your system CA file automatically.
+
+## capath
+
+If cafile is not specified or if the certificate is not found there, the
+directory pointed to by capath is searched for a suitable certificate.
+capath must be a correctly hashed certificate directory.
 
 ## http-basic
 

+ 1 - 1
doc/articles/custom-installers.md

@@ -84,7 +84,7 @@ Example:
         "class": "phpDocumentor\\Composer\\TemplateInstallerPlugin"
     },
     "require": {
-        "composer-plugin-api": "1.0.0"
+        "composer-plugin-api": "^1.0"
     }
 }
 ```

+ 48 - 3
doc/articles/plugins.md

@@ -36,7 +36,7 @@ as a normal package's.
 
 The current composer plugin API version is 1.0.0.
 
-An example of a valid plugin `composer.json` file (with the autoloading 
+An example of a valid plugin `composer.json` file (with the autoloading
 part omitted):
 
 ```json
@@ -89,9 +89,54 @@ Furthermore plugins may implement the
 event handlers automatically registered with the `EventDispatcher` when the
 plugin is loaded.
 
-Plugin can subscribe to any of the available [script events](scripts.md#event-names).
+To register a method to an event, implement the method `getSubscribedEvents()`
+and have it return an array. The array key must be the
+[event name](https://getcomposer.org/doc/articles/scripts.md#event-names)
+and the value is the name of the method in this class to be called.
 
-Example:
+```php
+public static function getSubscribedEvents()
+{
+    return array(
+        'post-autoload-dump' => 'methodToBeCalled',
+        // ^ event name ^         ^ method name ^
+    );
+}
+```
+
+By default, the priority of an event handler is set to 0. The priorty can be
+changed by attaching a tuple where the first value is the method name, as
+before, and the second value is an integer representing the priority.
+Higher integers represent higher priorities. Priortity 2 is called before
+priority 1, etc.
+
+```php
+public static function getSubscribedEvents()
+{
+    return array(
+        // Will be called before events with priority 0
+        'post-autoload-dump' => array('methodToBeCalled', 1)
+    );
+}
+```
+
+If multiple methods should be called, then an array of tuples can be attached
+to each event. The tuples do not need to include the priority. If it is
+omitted, it will default to 0.
+
+```php
+public static function getSubscribedEvents()
+{
+    return array(
+        'post-autoload-dump' => array(
+            array('methodToBeCalled'      ), // Priority defaults to 0
+            array('someOtherMethodName', 1), // This fires first
+        )
+    );
+}
+```
+
+Here's a complete example:
 
 ```php
 <?php

+ 4 - 4
doc/articles/troubleshooting.md

@@ -76,16 +76,16 @@ This is a list of common pitfalls on using Composer, and how to avoid them.
 
 ## I have a dependency which contains a "repositories" definition in its composer.json, but it seems to be ignored.
 
-The [`repositories`](04-schema.md#repositories) configuration property is defined as [root-only]
-(04-schema.md#root-package). It is not inherited. You can read more about the reasons behind this in the "[why can't
-composer load repositories recursively?](articles/why-can't-composer-load-repositories-recursively.md)" article.
+The [`repositories`](../04-schema.md#repositories) configuration property is defined as [root-only]
+(../04-schema.md#root-package). It is not inherited. You can read more about the reasons behind this in the "[why can't
+composer load repositories recursively?](../faqs/why-can't-composer-load-repositories-recursively.md)" article.
 The simplest work-around to this limitation, is moving or duplicating the `repositories` definition into your root
 composer.json.
 
 ## I have locked a dependency to a specific commit but get unexpected results.
 
 While Composer supports locking dependencies to a specific commit using the `#commit-ref` syntax, there are certain
-caveats that one should take into account. The most important one is [documented](04-schema.md#package-links), but
+caveats that one should take into account. The most important one is [documented](../04-schema.md#package-links), but
 frequently overlooked:
 
 > **Note:** While this is convenient at times, it should not be how you use

+ 4 - 0
res/composer-schema.json

@@ -149,6 +149,10 @@
                     "type": "string",
                     "description": "A way to set the path to the openssl CA file. In PHP 5.6+ you should rather set this via openssl.cafile in php.ini, although PHP 5.6+ should be able to detect your system CA file automatically."
                 },
+                "capath": {
+                    "type": "string",
+                    "description": "If cafile is not specified or if the certificate is not found there, the directory pointed to by capath is searched for a suitable certificate. capath must be a correctly hashed certificate directory."
+                },
                 "http-basic": {
                     "type": "object",
                     "description": "A hash of domain name => {\"username\": \"...\", \"password\": \"...\"}.",

+ 2 - 1
src/Composer/Autoload/ClassMapGenerator.php

@@ -18,6 +18,7 @@
 
 namespace Composer\Autoload;
 
+use Composer\Util\Silencer;
 use Symfony\Component\Finder\Finder;
 use Composer\IO\IOInterface;
 
@@ -122,7 +123,7 @@ class ClassMapGenerator
         }
 
         try {
-            $contents = @php_strip_whitespace($path);
+            $contents = Silencer::call('php_strip_whitespace', $path);
             if (!$contents) {
                 if (!file_exists($path)) {
                     throw new \Exception('File does not exist');

+ 6 - 13
src/Composer/Cache.php

@@ -14,6 +14,7 @@ namespace Composer;
 
 use Composer\IO\IOInterface;
 use Composer\Util\Filesystem;
+use Composer\Util\Silencer;
 use Symfony\Component\Finder\Finder;
 
 /**
@@ -44,7 +45,7 @@ class Cache
         $this->filesystem = $filesystem ?: new Filesystem();
 
         if (
-            (!is_dir($this->root) && !@mkdir($this->root, 0777, true))
+            (!is_dir($this->root) && !Silencer::call('mkdir', $this->root, 0777, true))
             || !is_writable($this->root)
         ) {
             $this->io->writeError('<warning>Cannot create cache directory ' . $this->root . ', or directory is not writable. Proceeding without cache</warning>');
@@ -66,9 +67,7 @@ class Cache
     {
         $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file);
         if ($this->enabled && file_exists($this->root . $file)) {
-            if ($this->io->isDebug()) {
-                $this->io->writeError('Reading '.$this->root . $file.' from cache');
-            }
+            $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG);
 
             return file_get_contents($this->root . $file);
         }
@@ -81,16 +80,12 @@ class Cache
         if ($this->enabled) {
             $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file);
 
-            if ($this->io->isDebug()) {
-                $this->io->writeError('Writing '.$this->root . $file.' into cache');
-            }
+            $this->io->writeError('Writing '.$this->root . $file.' into cache', true, IOInterface::DEBUG);
 
             try {
                 return file_put_contents($this->root . $file, $contents);
             } catch (\ErrorException $e) {
-                if ($this->io->isDebug()) {
-                    $this->io->writeError('<warning>Failed to write into cache: '.$e->getMessage().'</warning>');
-                }
+                $this->io->writeError('<warning>Failed to write into cache: '.$e->getMessage().'</warning>', true, IOInterface::DEBUG);
                 if (preg_match('{^file_put_contents\(\): Only ([0-9]+) of ([0-9]+) bytes written}', $e->getMessage(), $m)) {
                     // Remove partial file.
                     unlink($this->root . $file);
@@ -151,9 +146,7 @@ class Cache
                 touch($this->root . $file);
             }
 
-            if ($this->io->isDebug()) {
-                $this->io->writeError('Reading '.$this->root . $file.' from cache');
-            }
+            $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG);
 
             return copy($this->root . $file, $target);
         }

+ 25 - 10
src/Composer/Command/ConfigCommand.php

@@ -12,6 +12,8 @@
 
 namespace Composer\Command;
 
+use Composer\Util\Platform;
+use Composer\Util\Silencer;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputOption;
@@ -142,7 +144,7 @@ EOT
             ? ($this->config->get('home') . '/config.json')
             : ($input->getOption('file') ?: trim(getenv('COMPOSER')) ?: 'composer.json');
 
-        // create global composer.json if this was invoked using `composer global config`
+        // Create global composer.json if this was invoked using `composer global config`
         if ($configFile === 'composer.json' && !file_exists($configFile) && realpath(getcwd()) === realpath($this->config->get('home'))) {
             file_put_contents($configFile, "{\n}\n");
         }
@@ -157,16 +159,16 @@ EOT
         $this->authConfigFile = new JsonFile($authConfigFile, null, $io);
         $this->authConfigSource = new JsonConfigSource($this->authConfigFile, true);
 
-        // initialize the global file if it's not there
+        // Initialize the global file if it's not there, ignoring any warnings or notices
         if ($input->getOption('global') && !$this->configFile->exists()) {
             touch($this->configFile->getPath());
             $this->configFile->write(array('config' => new \ArrayObject));
-            @chmod($this->configFile->getPath(), 0600);
+            Silencer::call('chmod', $this->configFile->getPath(), 0600);
         }
         if ($input->getOption('global') && !$this->authConfigFile->exists()) {
             touch($this->authConfigFile->getPath());
             $this->authConfigFile->write(array('http-basic' => new \ArrayObject, 'github-oauth' => new \ArrayObject, 'gitlab-oauth' => new \ArrayObject));
-            @chmod($this->authConfigFile->getPath(), 0600);
+            Silencer::call('chmod', $this->authConfigFile->getPath(), 0600);
         }
 
         if (!$this->configFile->exists()) {
@@ -183,7 +185,7 @@ EOT
         if ($input->getOption('editor')) {
             $editor = escapeshellcmd(getenv('EDITOR'));
             if (!$editor) {
-                if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+                if (Platform::isWindows()) {
                     $editor = 'notepad';
                 } else {
                     foreach (array('vim', 'vi', 'nano', 'pico', 'ed') as $candidate) {
@@ -196,7 +198,7 @@ EOT
             }
 
             $file = $input->getOption('auth') ? $this->authConfigFile->getPath() : $this->configFile->getPath();
-            system($editor . ' ' . $file . (defined('PHP_WINDOWS_VERSION_BUILD') ? '' : ' > `tty`'));
+            system($editor . ' ' . $file . (Platform::isWindows() ? '' : ' > `tty`'));
 
             return 0;
         }
@@ -331,7 +333,11 @@ EOT
             'disable-tls' => array($booleanValidator, $booleanNormalizer),
             'cafile' => array(
                 function ($val) { return file_exists($val) && is_readable($val); },
-                function ($val) { return $val === 'null' ? null : $val; }
+                function ($val) { return $val === 'null' ? null : $val; },
+            ),
+            'capath' => array(
+                function ($val) { return is_dir($val) && is_readable($val); },
+                function ($val) { return $val === 'null' ? null : $val; },
             ),
             'github-expose-hostname' => array($booleanValidator, $booleanNormalizer),
         );
@@ -434,9 +440,18 @@ EOT
             }
 
             if (1 === count($values)) {
-                $bool = strtolower($values[0]);
-                if (true === $booleanValidator($bool) && false === $booleanNormalizer($bool)) {
-                    return $this->configSource->addRepository($matches[1], false);
+                $value = strtolower($values[0]);
+                if (true === $booleanValidator($value)) {
+                    if (false === $booleanNormalizer($value)) {
+                        return $this->configSource->addRepository($matches[1], false);
+                    }
+                } else {
+                    $value = json_decode($values[0], true);
+                    if (JSON_ERROR_NONE !== json_last_error()) {
+                        throw new \InvalidArgumentException(sprintf('%s is not valid JSON.', $values[0]));
+                    }
+
+                    return $this->configSource->addRepository($matches[1], $value);
                 }
             }
 

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

@@ -27,6 +27,7 @@ use Composer\Repository\CompositeRepository;
 use Composer\Repository\FilesystemRepository;
 use Composer\Repository\InstalledFilesystemRepository;
 use Composer\Script\ScriptEvents;
+use Composer\Util\Silencer;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
@@ -35,7 +36,6 @@ use Symfony\Component\Finder\Finder;
 use Composer\Json\JsonFile;
 use Composer\Config\JsonConfigSource;
 use Composer\Util\Filesystem;
-use Composer\Util\RemoteFilesystem;
 use Composer\Package\Version\VersionParser;
 
 /**
@@ -224,10 +224,10 @@ EOT
         chdir($oldCwd);
         $vendorComposerDir = $composer->getConfig()->get('vendor-dir').'/composer';
         if (is_dir($vendorComposerDir) && $fs->isDirEmpty($vendorComposerDir)) {
-            @rmdir($vendorComposerDir);
+            Silencer::call('rmdir', $vendorComposerDir);
             $vendorDir = $composer->getConfig()->get('vendor-dir');
             if (is_dir($vendorDir) && $fs->isDirEmpty($vendorDir)) {
-                @rmdir($vendorDir);
+                Silencer::call('rmdir', $vendorDir);
             }
         }
 
@@ -294,7 +294,7 @@ EOT
 
         // handler Ctrl+C for unix-like systems
         if (function_exists('pcntl_signal')) {
-            declare (ticks = 100);
+            declare(ticks=100);
             pcntl_signal(SIGINT, function () use ($directory) {
                 $fs = new Filesystem();
                 $fs->removeDirectory($directory);

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

@@ -132,7 +132,7 @@ EOT
         } else {
             $matchText = '';
             if ($input->getOption('match-constraint') !== '*') {
-                $matchText = ' in versions '.($matchInvert ? 'not ':'').'matching ' . $input->getOption('match-constraint');
+                $matchText = ' in versions '.($matchInvert ? 'not ' : '').'matching ' . $input->getOption('match-constraint');
             }
             $io->writeError('<info>There is no installed package depending on "'.$needle.'"'.$matchText.'.</info>');
         }

+ 33 - 0
src/Composer/Command/DiagnoseCommand.php

@@ -22,6 +22,7 @@ use Composer\Util\ConfigValidator;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\RemoteFilesystem;
 use Composer\Util\StreamContextFactory;
+use Composer\Util\Keys;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 
@@ -133,6 +134,9 @@ EOT
         $io->write('Checking disk free space: ', false);
         $this->outputResult($this->checkDiskSpace($config));
 
+        $io->write('Checking pubkeys: ', false);
+        $this->outputResult($this->checkPubKeys($config));
+
         $io->write('Checking composer version: ', false);
         $this->outputResult($this->checkVersion());
 
@@ -327,6 +331,35 @@ EOT
         return true;
     }
 
+    private function checkPubKeys($config)
+    {
+        $home = $config->get('home');
+        $errors = array();
+        $io = $this->getIO();
+
+        if (file_exists($home.'/keys.tags.pub') && file_exists($home.'/keys.dev.pub')) {
+            $io->write('');
+        }
+
+        if (file_exists($home.'/keys.tags.pub')) {
+            $io->write('Tags Public Key Fingerprint: ' . Keys::fingerprint($home.'/keys.tags.pub'));
+        } else {
+            $errors[] = '<error>Missing pubkey for tags verification</error>';
+        }
+
+        if (file_exists($home.'/keys.dev.pub')) {
+            $io->write('Dev Public Key Fingerprint: ' . Keys::fingerprint($home.'/keys.dev.pub'));
+        } else {
+            $errors[] = '<error>Missing pubkey for dev verification</error>';
+        }
+
+        if ($errors) {
+            $errors[] = '<error>Run composer self-update --update-keys to set them up</error>';
+        }
+
+        return $errors ?: true;
+    }
+
     private function checkVersion()
     {
         $protocol = extension_loaded('openssl') ? 'https' : 'http';

+ 2 - 1
src/Composer/Command/HomeCommand.php

@@ -16,6 +16,7 @@ use Composer\Factory;
 use Composer\Package\CompletePackageInterface;
 use Composer\Repository\RepositoryInterface;
 use Composer\Repository\ArrayRepository;
+use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputOption;
@@ -117,7 +118,7 @@ EOT
     {
         $url = ProcessExecutor::escape($url);
 
-        if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+        if (Platform::isWindows()) {
             return passthru('start "web" explorer "' . $url . '"');
         }
 

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

@@ -37,6 +37,7 @@ class RemoveCommand extends Command
             ->setDefinition(array(
                 new InputArgument('packages', InputArgument::IS_ARRAY, 'Packages that should be removed.'),
                 new InputOption('dev', null, InputOption::VALUE_NONE, 'Removes a package from the require-dev section.'),
+                new InputOption('no-plugins', null, InputOption::VALUE_NONE, 'Disables all plugins.'),
                 new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
                 new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'),
                 new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'),
@@ -92,7 +93,7 @@ EOT
         }
 
         // Update packages
-        $composer = $this->getComposer();
+        $composer = $this->getComposer(true, $input->getOption('no-plugins'));
         $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
 
         $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output);

+ 3 - 2
src/Composer/Command/RequireCommand.php

@@ -42,6 +42,7 @@ class RequireCommand extends InitCommand
                 new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'),
                 new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'),
                 new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist even for dev versions.'),
+                new InputOption('no-plugins', null, InputOption::VALUE_NONE, 'Disables all plugins.'),
                 new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
                 new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'),
                 new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'),
@@ -93,7 +94,7 @@ EOT
         $composerDefinition = $json->read();
         $composerBackup = file_get_contents($json->getPath());
 
-        $composer = $this->getComposer();
+        $composer = $this->getComposer(true, $input->getOption('no-plugins'));
         $repos = $composer->getRepositoryManager()->getRepositories();
 
         $platformOverrides = $composer->getConfig()->get('platform') ?: array();
@@ -143,7 +144,7 @@ EOT
 
         // Update packages
         $this->resetComposer();
-        $composer = $this->getComposer();
+        $composer = $this->getComposer(true, $input->getOption('no-plugins'));
         $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
 
         $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output);

+ 121 - 11
src/Composer/Command/SelfUpdateCommand.php

@@ -14,8 +14,10 @@ namespace Composer\Command;
 
 use Composer\Composer;
 use Composer\Factory;
+use Composer\Config;
 use Composer\Util\Filesystem;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\Keys;
+use Composer\IO\IOInterface;
 use Composer\Downloader\FilesystemException;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
@@ -44,6 +46,7 @@ class SelfUpdateCommand extends Command
                 new InputOption('clean-backups', null, InputOption::VALUE_NONE, 'Delete old backups during an update. This makes the current version of composer the only backup available after the update'),
                 new InputArgument('version', InputArgument::OPTIONAL, 'The version to update to'),
                 new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
+                new InputOption('update-keys', null, InputOption::VALUE_NONE, 'Prompt user for a key update'),
             ))
             ->setHelp(<<<EOT
 The <info>self-update</info> command checks getcomposer.org for newer
@@ -71,8 +74,13 @@ EOT
 
         $cacheDir = $config->get('cache-dir');
         $rollbackDir = $config->get('data-dir');
+        $home = $config->get('home');
         $localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0];
 
+        if ($input->getOption('update-keys')) {
+            return $this->fetchKeys($io, $config);
+        }
+
         // check if current dir is writable and if not try the cache dir from settings
         $tmpDir = is_writable(dirname($localFilename)) ? dirname($localFilename) : $cacheDir;
 
@@ -80,9 +88,6 @@ EOT
         if (!is_writable($tmpDir)) {
             throw new FilesystemException('Composer update failed: the "'.$tmpDir.'" directory used to download the temp file could not be written');
         }
-        if (!is_writable($localFilename)) {
-            throw new FilesystemException('Composer update failed: the "'.$localFilename.'" file could not be written');
-        }
 
         if ($input->getOption('rollback')) {
             return $this->rollback($output, $rollbackDir, $localFilename);
@@ -112,15 +117,79 @@ EOT
             self::OLD_INSTALL_EXT
         );
 
-        $io->writeError(sprintf("Updating to version <info>%s</info>.", $updateVersion));
-        $remoteFilename = $baseUrl . (preg_match('{^[0-9a-f]{40}$}', $updateVersion) ? '/composer.phar' : "/download/{$updateVersion}/composer.phar");
+        $updatingToTag = !preg_match('{^[0-9a-f]{40}$}', $updateVersion);
+
+        $io->write(sprintf("Updating to version <info>%s</info>.", $updateVersion));
+        $remoteFilename = $baseUrl . ($updatingToTag ? "/download/{$updateVersion}/composer.phar" : '/composer.phar');
+        $signature = $remoteFilesystem->getContents(self::HOMEPAGE, $remoteFilename.'.sig', false);
         $remoteFilesystem->copy(self::HOMEPAGE, $remoteFilename, $tempFilename, !$input->getOption('no-progress'));
-        if (!file_exists($tempFilename)) {
+        if (!file_exists($tempFilename) || !$signature) {
             $io->writeError('<error>The download of the new composer version failed for an unexpected reason</error>');
 
             return 1;
         }
 
+        // verify phar signature
+        if (!extension_loaded('openssl') && $config->get('disable-tls')) {
+            $io->writeError('<warning>Skipping phar signature verification as you have disabled OpenSSL via config.disable-tls</warning>');
+        } else {
+            if (!extension_loaded('openssl')) {
+                throw new \RuntimeException('The openssl extension is required for phar signatures to be verified but it 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.');
+            }
+
+            $sigFile = 'file://'.$home.'/' . ($updatingToTag ? 'keys.tags.pub' : 'keys.dev.pub');
+            if (!file_exists($sigFile)) {
+                file_put_contents($home.'/keys.dev.pub', <<<DEVPUBKEY
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnBDHjZS6e0ZMoK3xTD7f
+FNCzlXjX/Aie2dit8QXA03pSrOTbaMnxON3hUL47Lz3g1SC6YJEMVHr0zYq4elWi
+i3ecFEgzLcj+pZM5X6qWu2Ozz4vWx3JYo1/a/HYdOuW9e3lwS8VtS0AVJA+U8X0A
+hZnBmGpltHhO8hPKHgkJtkTUxCheTcbqn4wGHl8Z2SediDcPTLwqezWKUfrYzu1f
+o/j3WFwFs6GtK4wdYtiXr+yspBZHO3y1udf8eFFGcb2V3EaLOrtfur6XQVizjOuk
+8lw5zzse1Qp/klHqbDRsjSzJ6iL6F4aynBc6Euqt/8ccNAIz0rLjLhOraeyj4eNn
+8iokwMKiXpcrQLTKH+RH1JCuOVxQ436bJwbSsp1VwiqftPQieN+tzqy+EiHJJmGf
+TBAbWcncicCk9q2md+AmhNbvHO4PWbbz9TzC7HJb460jyWeuMEvw3gNIpEo2jYa9
+pMV6cVqnSa+wOc0D7pC9a6bne0bvLcm3S+w6I5iDB3lZsb3A9UtRiSP7aGSo7D72
+8tC8+cIgZcI7k9vjvOqH+d7sdOU2yPCnRY6wFh62/g8bDnUpr56nZN1G89GwM4d4
+r/TU7BQQIzsZgAiqOGXvVklIgAMiV0iucgf3rNBLjjeNEwNSTTG9F0CtQ+7JLwaE
+wSEuAuRm+pRqi8BRnQ/GKUcCAwEAAQ==
+-----END PUBLIC KEY-----
+DEVPUBKEY
+);
+                file_put_contents($home.'/keys.tags.pub', <<<TAGSPUBKEY
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0Vi/2K6apCVj76nCnCl2
+MQUPdK+A9eqkYBacXo2wQBYmyVlXm2/n/ZsX6pCLYPQTHyr5jXbkQzBw8SKqPdlh
+vA7NpbMeNCz7wP/AobvUXM8xQuXKbMDTY2uZ4O7sM+PfGbptKPBGLe8Z8d2sUnTO
+bXtX6Lrj13wkRto7st/w/Yp33RHe9SlqkiiS4MsH1jBkcIkEHsRaveZzedUaxY0M
+mba0uPhGUInpPzEHwrYqBBEtWvP97t2vtfx8I5qv28kh0Y6t+jnjL1Urid2iuQZf
+noCMFIOu4vksK5HxJxxrN0GOmGmwVQjOOtxkwikNiotZGPR4KsVj8NnBrLX7oGuM
+nQvGciiu+KoC2r3HDBrpDeBVdOWxDzT5R4iI0KoLzFh2pKqwbY+obNPS2bj+2dgJ
+rV3V5Jjry42QOCBN3c88wU1PKftOLj2ECpewY6vnE478IipiEu7EAdK8Zwj2LmTr
+RKQUSa9k7ggBkYZWAeO/2Ag0ey3g2bg7eqk+sHEq5ynIXd5lhv6tC5PBdHlWipDK
+tl2IxiEnejnOmAzGVivE1YGduYBjN+mjxDVy8KGBrjnz1JPgAvgdwJ2dYw4Rsc/e
+TzCFWGk/HM6a4f0IzBWbJ5ot0PIi4amk07IotBXDWwqDiQTwyuGCym5EqWQ2BD95
+RGv89BPD+2DLnJysngsvVaUCAwEAAQ==
+-----END PUBLIC KEY-----
+TAGSPUBKEY
+);
+            }
+
+            $pubkeyid = openssl_pkey_get_public($sigFile);
+            $algo = defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'SHA384';
+            if (!in_array('SHA384', openssl_get_md_methods())) {
+                throw new \RuntimeException('SHA384 is not supported by your openssl extension, could not verify the phar file integrity');
+            }
+            $signature = json_decode($signature, true);
+            $signature = base64_decode($signature['sha384']);
+            $verified = 1 === openssl_verify(file_get_contents($tempFilename), $signature, $pubkeyid, $algo);
+            openssl_free_key($pubkeyid);
+            if (!$verified) {
+                throw new \RuntimeException('The phar signature did not match the file you downloaded, this means your public keys are outdated or that the phar file is corrupt/has been modified');
+            }
+        }
+
         // remove saved installations of composer
         if ($input->getOption('clean-backups')) {
             $finder = $this->getOldInstallationFinder($rollbackDir);
@@ -147,6 +216,51 @@ EOT
         }
     }
 
+    protected function fetchKeys(IOInterface $io, Config $config)
+    {
+        if (!$io->isInteractive()) {
+            throw new \RuntimeException('Public keys can not be fetched in non-interactive mode, please run Composer interactively');
+        }
+
+        $io->write('Open <info>https://composer.github.io/pubkeys.html</info> to find the latest keys');
+
+        $validator = function ($value) {
+            if (!preg_match('{^-----BEGIN PUBLIC KEY-----$}', trim($value))) {
+                throw new \UnexpectedValueException('Invalid input');
+            }
+
+            return trim($value)."\n";
+        };
+
+        $devKey = '';
+        while (!preg_match('{(-----BEGIN PUBLIC KEY-----.+?-----END PUBLIC KEY-----)}s', $devKey, $match)) {
+            $devKey = $io->askAndValidate('Enter Dev / Snapshot Public Key (including lines with -----): ', $validator);
+            while ($line = $io->ask('')) {
+                $devKey .= trim($line)."\n";
+                if (trim($line) === '-----END PUBLIC KEY-----') {
+                    break;
+                }
+            }
+        }
+        file_put_contents($keyPath = $config->get('home').'/keys.dev.pub', $match[0]);
+        $io->write('Stored key with fingerprint: ' . Keys::fingerprint($keyPath));
+
+        $tagsKey = '';
+        while (!preg_match('{(-----BEGIN PUBLIC KEY-----.+?-----END PUBLIC KEY-----)}s', $tagsKey, $match)) {
+            $tagsKey = $io->askAndValidate('Enter Tags Public Key (including lines with -----): ', $validator);
+            while ($line = $io->ask('')) {
+                $tagsKey .= trim($line)."\n";
+                if (trim($line) === '-----END PUBLIC KEY-----') {
+                    break;
+                }
+            }
+        }
+        file_put_contents($keyPath = $config->get('home').'/keys.tags.pub', $match[0]);
+        $io->write('Stored key with fingerprint: ' . Keys::fingerprint($keyPath));
+
+        $io->write('Public keys stored in '.$config->get('home'));
+    }
+
     protected function rollback(OutputInterface $output, $rollbackDir, $localFilename)
     {
         $rollbackVersion = $this->getLastBackupVersion($rollbackDir);
@@ -154,10 +268,6 @@ EOT
             throw new \UnexpectedValueException('Composer rollback failed: no installation to roll back to in "'.$rollbackDir.'"');
         }
 
-        if (!is_writable($rollbackDir)) {
-            throw new FilesystemException('Composer rollback failed: the "'.$rollbackDir.'" dir could not be written to');
-        }
-
         $old = $rollbackDir . '/' . $rollbackVersion . self::OLD_INSTALL_EXT;
 
         if (!is_file($old)) {

+ 24 - 23
src/Composer/Command/ShowCommand.php

@@ -20,6 +20,7 @@ use Composer\Semver\VersionParser;
 use Composer\Plugin\CommandEvent;
 use Composer\Plugin\PluginEvents;
 use Composer\Package\PackageInterface;
+use Composer\Util\Platform;
 use Symfony\Component\Console\Formatter\OutputFormatterStyle;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputArgument;
@@ -232,7 +233,7 @@ EOT
                     // outside of a real terminal, use space without a limit
                     $width = PHP_INT_MAX;
                 }
-                if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+                if (Platform::isWindows()) {
                     $width--;
                 }
 
@@ -246,10 +247,10 @@ EOT
                 $writeDescription = !$input->getOption('name-only') && !$input->getOption('path') && ($nameLength + ($showVersion ? $versionLength : 0) + 24 <= $width);
                 foreach ($packages[$type] as $package) {
                     if (is_object($package)) {
-                        $output->write($indent . str_pad($package->getPrettyName(), $nameLength, ' '), false);
+                        $io->write($indent . str_pad($package->getPrettyName(), $nameLength, ' '), false);
 
                         if ($writeVersion) {
-                            $output->write(' ' . str_pad($package->getFullPrettyVersion(), $versionLength, ' '), false);
+                            $io->write(' ' . str_pad($package->getFullPrettyVersion(), $versionLength, ' '), false);
                         }
 
                         if ($writeDescription) {
@@ -258,15 +259,15 @@ EOT
                             if (strlen($description) > $remaining) {
                                 $description = substr($description, 0, $remaining - 3) . '...';
                             }
-                            $output->write(' ' . $description);
+                            $io->write(' ' . $description, false);
                         }
 
                         if ($writePath) {
                             $path = strtok(realpath($composer->getInstallationManager()->getInstallPath($package)), "\r\n");
-                            $output->write(' ' . $path);
+                            $io->write(' ' . $path, false);
                         }
                     } else {
-                        $output->write($indent . $package);
+                        $io->write($indent . $package, false);
                     }
                     $io->write('');
                 }
@@ -458,7 +459,7 @@ EOT
     /**
      * Init styles for tree
      *
-     * @param  OutputInterface $output
+     * @param OutputInterface $output
      */
     protected function initStyles(OutputInterface $output)
     {
@@ -479,20 +480,20 @@ EOT
     /**
      * Display the tree
      *
-     * @param  PackageInterface|string $package
-     * @param  RepositoryInterface     $installedRepo
-     * @param  RepositoryInterface     $distantRepos
-     * @param  OutputInterface         $output
+     * @param PackageInterface|string $package
+     * @param RepositoryInterface     $installedRepo
+     * @param RepositoryInterface     $distantRepos
+     * @param OutputInterface         $output
      */
     protected function displayPackageTree(PackageInterface $package, RepositoryInterface $installedRepo, RepositoryInterface $distantRepos, OutputInterface $output)
     {
         $packagesInTree = array();
         $packagesInTree[] = $package;
 
-        $output->write(sprintf('<info>%s</info>', $package->getPrettyName()));
-        $output->write(' ' . $package->getPrettyVersion());
-        $output->write(' ' . strtok($package->getDescription(), "\r\n"));
-        $output->writeln('');
+        $io = $this->getIO();
+        $io->write(sprintf('<info>%s</info>', $package->getPrettyName()), false);
+        $io->write(' ' . $package->getPrettyVersion(), false);
+        $io->write(' ' . strtok($package->getDescription(), "\r\n"));
 
         if (is_object($package)) {
             $requires = $package->getRequires();
@@ -524,14 +525,14 @@ EOT
     /**
      * Display a package tree
      *
-     * @param  string                  $name
-     * @param  PackageInterface|string $package
-     * @param  RepositoryInterface     $installedRepo
-     * @param  RepositoryInterface     $distantRepos
-     * @param  array                   $packagesInTree
-     * @param  OutputInterface         $output
-     * @param  string                  $previousTreeBar
-     * @param  integer                 $level
+     * @param string                  $name
+     * @param PackageInterface|string $package
+     * @param RepositoryInterface     $installedRepo
+     * @param RepositoryInterface     $distantRepos
+     * @param array                   $packagesInTree
+     * @param OutputInterface         $output
+     * @param string                  $previousTreeBar
+     * @param int                     $level
      */
     protected function displayTree($name, $package, RepositoryInterface $installedRepo, RepositoryInterface $distantRepos, array $packagesInTree, OutputInterface $output, $previousTreeBar = '├', $level = 1)
     {

+ 4 - 2
src/Composer/Config.php

@@ -47,6 +47,7 @@ class Config
         'github-domains' => array('github.com'),
         'disable-tls' => false,
         'cafile' => null,
+        'capath' => null,
         'github-expose-hostname' => true,
         'gitlab-domains' => array('gitlab.com'),
         'store-auths' => 'prompt',
@@ -179,6 +180,7 @@ class Config
             case 'cache-repo-dir':
             case 'cache-vcs-dir':
             case 'cafile':
+            case 'capath':
                 // convert foo-bar to COMPOSER_FOO_BAR and check if it exists since it overrides the local config
                 $env = 'COMPOSER_' . strtoupper(strtr($key, '-', '_'));
 
@@ -189,7 +191,7 @@ class Config
                     return $val;
                 }
 
-                return ($flags & self::RELATIVE_PATHS == self::RELATIVE_PATHS) ? $val : $this->realpath($val);
+                return (($flags & self::RELATIVE_PATHS) == self::RELATIVE_PATHS) ? $val : $this->realpath($val);
 
             case 'cache-ttl':
                 return (int) $this->config[$key];
@@ -343,7 +345,7 @@ class Config
      */
     private function realpath($path)
     {
-        if (substr($path, 0, 1) === '/' || substr($path, 1, 1) === ':') {
+        if (preg_match('{^(?:/|[a-z]:|[a-z0-9.]+://)}i', $path)) {
             return $path;
         }
 

+ 2 - 1
src/Composer/Config/JsonConfigSource.php

@@ -14,6 +14,7 @@ namespace Composer\Config;
 
 use Composer\Json\JsonFile;
 use Composer\Json\JsonManipulator;
+use Composer\Util\Silencer;
 
 /**
  * JSON Configuration Source
@@ -173,7 +174,7 @@ class JsonConfigSource implements ConfigSourceInterface
         }
 
         if ($newFile) {
-            @chmod($this->file->getPath(), 0600);
+            Silencer::call('chmod', $this->file->getPath(), 0600);
         }
     }
 

+ 15 - 14
src/Composer/Console/Application.php

@@ -12,10 +12,11 @@
 
 namespace Composer\Console;
 
+use Composer\Util\Platform;
+use Composer\Util\Silencer;
 use Symfony\Component\Console\Application as BaseApplication;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
-use Symfony\Component\Console\Output\ConsoleOutputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Output\ConsoleOutput;
 use Symfony\Component\Console\Formatter\OutputFormatter;
@@ -64,7 +65,7 @@ class Application extends BaseApplication
         }
 
         if (function_exists('date_default_timezone_set') && function_exists('date_default_timezone_get')) {
-            date_default_timezone_set(@date_default_timezone_get());
+            date_default_timezone_set(Silencer::call('date_default_timezone_get'));
         }
 
         if (!$shutdownRegistered) {
@@ -136,9 +137,7 @@ class Application extends BaseApplication
             if ($newWorkDir = $this->getNewWorkingDir($input)) {
                 $oldWorkingDir = getcwd();
                 chdir($newWorkDir);
-                if ($io->isDebug() >= 4) {
-                    $io->writeError('Changed CWD to ' . getcwd());
-                }
+                $io->writeError('Changed CWD to ' . getcwd(), true, IOInterface::DEBUG);
             }
 
             // add non-standard scripts as own commands
@@ -203,30 +202,32 @@ class Application extends BaseApplication
     {
         $io = $this->getIO();
 
+        Silencer::suppress();
         try {
             $composer = $this->getComposer(false, true);
             if ($composer) {
                 $config = $composer->getConfig();
 
                 $minSpaceFree = 1024 * 1024;
-                if ((($df = @disk_free_space($dir = $config->get('home'))) !== false && $df < $minSpaceFree)
-                    || (($df = @disk_free_space($dir = $config->get('vendor-dir'))) !== false && $df < $minSpaceFree)
-                    || (($df = @disk_free_space($dir = sys_get_temp_dir())) !== false && $df < $minSpaceFree)
+                if ((($df = disk_free_space($dir = $config->get('home'))) !== false && $df < $minSpaceFree)
+                    || (($df = disk_free_space($dir = $config->get('vendor-dir'))) !== false && $df < $minSpaceFree)
+                    || (($df = disk_free_space($dir = sys_get_temp_dir())) !== false && $df < $minSpaceFree)
                 ) {
-                    $io->writeError('<error>The disk hosting '.$dir.' is full, this may be the cause of the following exception</error>');
+                    $io->writeError('<error>The disk hosting '.$dir.' is full, this may be the cause of the following exception</error>', true, IOInterface::QUIET);
                 }
             }
         } catch (\Exception $e) {
         }
+        Silencer::restore();
 
-        if (defined('PHP_WINDOWS_VERSION_BUILD') && false !== strpos($exception->getMessage(), 'The system cannot find the path specified')) {
-            $io->writeError('<error>The following exception may be caused by a stale entry in your cmd.exe AutoRun</error>');
-            $io->writeError('<error>Check https://getcomposer.org/doc/articles/troubleshooting.md#-the-system-cannot-find-the-path-specified-windows- for details</error>');
+        if (Platform::isWindows() && false !== strpos($exception->getMessage(), 'The system cannot find the path specified')) {
+            $io->writeError('<error>The following exception may be caused by a stale entry in your cmd.exe AutoRun</error>', true, IOInterface::QUIET);
+            $io->writeError('<error>Check https://getcomposer.org/doc/articles/troubleshooting.md#-the-system-cannot-find-the-path-specified-windows- for details</error>', true, IOInterface::QUIET);
         }
 
         if (false !== strpos($exception->getMessage(), 'fork failed - Cannot allocate memory')) {
-            $io->writeError('<error>The following exception is caused by a lack of memory and not having swap configured</error>');
-            $io->writeError('<error>Check https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors for details</error>');
+            $io->writeError('<error>The following exception is caused by a lack of memory and not having swap configured</error>', true, IOInterface::QUIET);
+            $io->writeError('<error>Check https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors for details</error>', true, IOInterface::QUIET);
         }
     }
 

+ 46 - 1
src/Composer/DependencyResolver/SolverProblemsException.php

@@ -31,12 +31,21 @@ class SolverProblemsException extends \RuntimeException
     protected function createMessage()
     {
         $text = "\n";
+        $hasExtensionProblems = false;
         foreach ($this->problems as $i => $problem) {
             $text .= "  Problem ".($i + 1).$problem->getPrettyString($this->installedMap)."\n";
+
+            if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) {
+                $hasExtensionProblems = true;
+            }
         }
 
         if (strpos($text, 'could not be found') || strpos($text, 'no matching package found')) {
-            $text .= "\nPotential causes:\n - A typo in the package name\n - The package is not available in a stable-enough version according to your minimum-stability setting\n   see <https://groups.google.com/d/topic/composer-dev/_g3ASeIFlrc/discussion> for more details.\n\nRead <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.";
+            $text .= "\nPotential causes:\n - A typo in the package name\n - The package is not available in a stable-enough version according to your minimum-stability setting\n   see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for more details.\n\nRead <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.";
+        }
+
+        if ($hasExtensionProblems) {
+            $text .= $this->createExtensionHint();
         }
 
         return $text;
@@ -46,4 +55,40 @@ class SolverProblemsException extends \RuntimeException
     {
         return $this->problems;
     }
+
+    private function createExtensionHint()
+    {
+        $paths = array();
+
+        if (($iniPath = php_ini_loaded_file()) !== false) {
+            $paths[] = $iniPath;
+        }
+
+        if (!defined('HHVM_VERSION') && $additionalIniPaths = php_ini_scanned_files()) {
+            $paths = array_merge($paths, array_map("trim", explode(",", $additionalIniPaths)));
+        }
+
+        if (count($paths) === 0) {
+            return '';
+        }
+
+        $text = "\n  To enable extensions, verify that they are enabled in those .ini files:\n    - ";
+        $text .= implode("\n    - ", $paths);
+        $text .= "\n  You can also run `php --ini` inside terminal to see which files are used by PHP in CLI mode.";
+
+        return $text;
+    }
+
+    private function hasExtensionProblems(array $reasonSets)
+    {
+        foreach ($reasonSets as $reasonSet) {
+            foreach ($reasonSet as $reason) {
+                if (isset($reason["rule"]) && 0 === strpos($reason["rule"]->getRequiredPackage(), 'ext-')) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
 }

+ 2 - 3
src/Composer/Downloader/ArchiveDownloader.php

@@ -14,6 +14,7 @@ namespace Composer\Downloader;
 
 use Composer\Package\PackageInterface;
 use Symfony\Component\Finder\Finder;
+use Composer\IO\IOInterface;
 
 /**
  * Base downloader for archives
@@ -34,9 +35,7 @@ abstract class ArchiveDownloader extends FileDownloader
         while ($retries--) {
             $fileName = parent::download($package, $path);
 
-            if ($this->io->isVerbose()) {
-                $this->io->writeError('    Extracting archive');
-            }
+            $this->io->writeError('    Extracting archive', true, IOInterface::VERBOSE);
 
             try {
                 $this->filesystem->ensureDirectoryExists($temporaryDir);

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

@@ -141,9 +141,7 @@ class FileDownloader implements DownloaderInterface
                         if ((0 !== $e->getCode() && !in_array($e->getCode(), array(500, 502, 503, 504))) || !$retries) {
                             throw $e;
                         }
-                        if ($this->io->isVerbose()) {
-                            $this->io->writeError('    Download failed, retrying...');
-                        }
+                        $this->io->writeError('    Download failed, retrying...', true, IOInterface::VERBOSE);
                         usleep(500000);
                     }
                 }

+ 3 - 2
src/Composer/Downloader/GitDownloader.php

@@ -14,6 +14,7 @@ namespace Composer\Downloader;
 
 use Composer\Package\PackageInterface;
 use Composer\Util\Git as GitUtil;
+use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\IO\IOInterface;
 use Composer\Util\Filesystem;
@@ -43,7 +44,7 @@ class GitDownloader extends VcsDownloader
         $path = $this->normalizePath($path);
 
         $ref = $package->getSourceReference();
-        $flag = defined('PHP_WINDOWS_VERSION_MAJOR') ? '/D ' : '';
+        $flag = Platform::isWindows() ? '/D ' : '';
         $command = 'git clone --no-checkout %s %s && cd '.$flag.'%2$s && git remote add composer %1$s && git fetch composer';
         $this->io->writeError("    Cloning ".$ref);
 
@@ -353,7 +354,7 @@ class GitDownloader extends VcsDownloader
 
     protected function normalizePath($path)
     {
-        if (defined('PHP_WINDOWS_VERSION_MAJOR') && strlen($path) > 0) {
+        if (Platform::isWindows() && strlen($path) > 0) {
             $basePath = $path;
             $removed = array();
 

+ 21 - 8
src/Composer/Downloader/GzipDownloader.php

@@ -16,6 +16,7 @@ use Composer\Config;
 use Composer\Cache;
 use Composer\EventDispatcher\EventDispatcher;
 use Composer\Package\PackageInterface;
+use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\RemoteFilesystem;
 use Composer\IO\IOInterface;
@@ -40,25 +41,26 @@ class GzipDownloader extends ArchiveDownloader
         $targetFilepath = $path . DIRECTORY_SEPARATOR . basename(substr($file, 0, -3));
 
         // Try to use gunzip on *nix
-        if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
+        if (!Platform::isWindows()) {
             $command = 'gzip -cd ' . ProcessExecutor::escape($file) . ' > ' . ProcessExecutor::escape($targetFilepath);
 
             if (0 === $this->process->execute($command, $ignoredOutput)) {
                 return;
             }
 
+            if (extension_loaded('zlib')) {
+                // Fallback to using the PHP extension.
+                $this->extractUsingExt($file, $targetFilepath);
+
+                return;
+            }
+
             $processError = 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput();
             throw new \RuntimeException($processError);
         }
 
         // Windows version of PHP has built-in support of gzip functions
-        $archiveFile = gzopen($file, 'rb');
-        $targetFile = fopen($targetFilepath, 'wb');
-        while ($string = gzread($archiveFile, 4096)) {
-            fwrite($targetFile, $string, strlen($string));
-        }
-        gzclose($archiveFile);
-        fclose($targetFile);
+        $this->extractUsingExt($file, $targetFilepath);
     }
 
     /**
@@ -68,4 +70,15 @@ class GzipDownloader extends ArchiveDownloader
     {
         return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
     }
+
+    private function extractUsingExt($file, $targetFilepath)
+    {
+        $archiveFile = gzopen($file, 'rb');
+        $targetFile = fopen($targetFilepath, 'wb');
+        while ($string = gzread($archiveFile, 4096)) {
+            fwrite($targetFile, $string, strlen($string));
+        }
+        gzclose($archiveFile);
+        fclose($targetFile);
+    }
 }

+ 3 - 2
src/Composer/Downloader/RarDownloader.php

@@ -15,6 +15,7 @@ namespace Composer\Downloader;
 use Composer\Config;
 use Composer\Cache;
 use Composer\EventDispatcher\EventDispatcher;
+use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\RemoteFilesystem;
 use Composer\IO\IOInterface;
@@ -42,7 +43,7 @@ class RarDownloader extends ArchiveDownloader
         $processError = null;
 
         // Try to use unrar on *nix
-        if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
+        if (!Platform::isWindows()) {
             $command = 'unrar x ' . ProcessExecutor::escape($file) . ' ' . ProcessExecutor::escape($path) . ' && chmod -R u+w ' . ProcessExecutor::escape($path);
 
             if (0 === $this->process->execute($command, $ignoredOutput)) {
@@ -65,7 +66,7 @@ class RarDownloader extends ArchiveDownloader
             $error = "Could not decompress the archive, enable the PHP rar extension or install unrar.\n"
                 . $iniMessage . "\n" . $processError;
 
-            if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
+            if (!Platform::isWindows()) {
                 $error = "Could not decompress the archive, enable the PHP rar extension.\n" . $iniMessage;
             }
 

+ 3 - 2
src/Composer/Downloader/ZipDownloader.php

@@ -15,6 +15,7 @@ namespace Composer\Downloader;
 use Composer\Config;
 use Composer\Cache;
 use Composer\EventDispatcher\EventDispatcher;
+use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\RemoteFilesystem;
 use Composer\IO\IOInterface;
@@ -38,7 +39,7 @@ class ZipDownloader extends ArchiveDownloader
         $processError = null;
 
         // try to use unzip on *nix
-        if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
+        if (!Platform::isWindows()) {
             $command = 'unzip '.ProcessExecutor::escape($file).' -d '.ProcessExecutor::escape($path) . ' && chmod -R u+w ' . ProcessExecutor::escape($path);
             try {
                 if (0 === $this->process->execute($command, $ignoredOutput)) {
@@ -64,7 +65,7 @@ class ZipDownloader extends ArchiveDownloader
             $error = "Could not decompress the archive, enable the PHP zip extension or install unzip.\n"
                 . $iniMessage . "\n" . $processError;
 
-            if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
+            if (!Platform::isWindows()) {
                 $error = "Could not decompress the archive, enable the PHP zip extension.\n" . $iniMessage;
             }
 

+ 1 - 3
src/Composer/EventDispatcher/EventDispatcher.php

@@ -155,9 +155,7 @@ class EventDispatcher
                 $event = $this->checkListenerExpectedEvent($callable, $event);
                 $return = false === call_user_func($callable, $event) ? 1 : 0;
             } elseif ($this->isComposerScript($callable)) {
-                if ($this->io->isVerbose()) {
-                    $this->io->writeError(sprintf('> %s: %s', $event->getName(), $callable));
-                }
+                $this->io->writeError(sprintf('> %s: %s', $event->getName(), $callable), true, IOInterface::VERBOSE);
                 $scriptName = substr($callable, 1);
                 $args = $event->getArguments();
                 $flags = $event->getFlags();

+ 37 - 24
src/Composer/Factory.php

@@ -20,8 +20,10 @@ use Composer\Package\Version\VersionGuesser;
 use Composer\Repository\RepositoryManager;
 use Composer\Repository\WritableRepositoryInterface;
 use Composer\Util\Filesystem;
+use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\RemoteFilesystem;
+use Composer\Util\Silencer;
 use Symfony\Component\Console\Formatter\OutputFormatterStyle;
 use Composer\EventDispatcher\EventDispatcher;
 use Composer\Autoload\AutoloadGenerator;
@@ -40,8 +42,8 @@ use Seld\JsonLint\JsonParser;
 class Factory
 {
     /**
-     * @return string
      * @throws \RuntimeException
+     * @return string
      */
     protected static function getHomeDir()
     {
@@ -50,7 +52,7 @@ class Factory
             return $home;
         }
 
-        if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+        if (Platform::isWindows()) {
             if (!getenv('APPDATA')) {
                 throw new \RuntimeException('The APPDATA or COMPOSER_HOME environment variable must be set for composer to run correctly');
             }
@@ -89,7 +91,7 @@ class Factory
             return $homeEnv . '/cache';
         }
 
-        if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+        if (Platform::isWindows()) {
             if ($cacheDir = getenv('LOCALAPPDATA')) {
                 $cacheDir .= '/Composer';
             } else {
@@ -114,7 +116,7 @@ class Factory
     }
 
     /**
-     * @param string $home
+     * @param  string $home
      * @return string
      */
     protected static function getDataDir($home)
@@ -124,7 +126,7 @@ class Factory
             return $homeEnv;
         }
 
-        if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+        if (Platform::isWindows()) {
             return strtr($home, '\\', '/');
         }
 
@@ -139,7 +141,7 @@ class Factory
     }
 
     /**
-     * @param IOInterface|null $io
+     * @param  IOInterface|null $io
      * @return Config
      */
     public static function createConfig(IOInterface $io = null, $cwd = null)
@@ -163,9 +165,9 @@ class Factory
         foreach ($dirs as $dir) {
             if (!file_exists($dir . '/.htaccess')) {
                 if (!is_dir($dir)) {
-                    @mkdir($dir, 0777, true);
+                    Silencer::call('mkdir', $dir, 0777, true);
                 }
-                @file_put_contents($dir . '/.htaccess', 'Deny from all');
+                Silencer::call('file_put_contents', $dir . '/.htaccess', 'Deny from all');
             }
         }
 
@@ -189,6 +191,20 @@ class Factory
         }
         $config->setAuthConfigSource(new JsonConfigSource($file, true));
 
+        // load COMPOSER_AUTH environment variable if set
+        if ($composerAuthEnv = getenv('COMPOSER_AUTH')) {
+            $authData = json_decode($composerAuthEnv, true);
+
+            if (is_null($authData)) {
+                throw new \UnexpectedValueException('COMPOSER_AUTH environment variable is malformed, should be a valid JSON object');
+            }
+
+            if ($io && $io->isDebug()) {
+                $io->writeError('Loading auth config from COMPOSER_AUTH');
+            }
+            $config->merge(array('config' => $authData));
+        }
+
         return $config;
     }
 
@@ -292,14 +308,10 @@ class Factory
         $config = static::createConfig($io, $cwd);
         $config->merge($localConfig);
         if (isset($composerFile)) {
-            if ($io && $io->isDebug()) {
-                $io->writeError('Loading config file ' . $composerFile);
-            }
+            $io->writeError('Loading config file ' . $composerFile, true, IOInterface::DEBUG);
             $localAuthFile = new JsonFile(dirname(realpath($composerFile)) . '/auth.json');
             if ($localAuthFile->exists()) {
-                if ($io && $io->isDebug()) {
-                    $io->writeError('Loading config file ' . $localAuthFile->getPath());
-                }
+                $io->writeError('Loading config file ' . $localAuthFile->getPath(), true, IOInterface::DEBUG);
                 $config->merge(array('config' => $localAuthFile->read()));
                 $config->setAuthConfigSource(new JsonConfigSource($localAuthFile, true));
             }
@@ -434,9 +446,7 @@ class Factory
         try {
             $composer = self::createComposer($io, $config->get('home') . '/composer.json', $disablePlugins, $config->get('home'), false);
         } catch (\Exception $e) {
-            if ($io->isDebug()) {
-                $io->writeError('Failed to initialize global composer: '.$e->getMessage());
-            }
+            $io->writeError('Failed to initialize global composer: '.$e->getMessage(), true, IOInterface::DEBUG);
         }
 
         return $composer;
@@ -568,9 +578,9 @@ class Factory
     }
 
     /**
-     * @param IOInterface   $io         IO instance
-     * @param Config        $config     Config instance
-     * @param array         $options    Array of options passed directly to RemoteFilesystem constructor
+     * @param  IOInterface      $io      IO instance
+     * @param  Config           $config  Config instance
+     * @param  array            $options Array of options passed directly to RemoteFilesystem constructor
      * @return RemoteFilesystem
      */
     public static function createRemoteFilesystem(IOInterface $io, Config $config = null, $options = array())
@@ -590,9 +600,12 @@ class Factory
         $remoteFilesystemOptions = array();
         if ($disableTls === false) {
             if ($config && $config->get('cafile')) {
-                $remoteFilesystemOptions = array('ssl' => array('cafile' => $config->get('cafile')));
+                $remoteFilesystemOptions['ssl']['cafile'] = $config->get('cafile');
+            }
+            if ($config && $config->get('capath')) {
+                $remoteFilesystemOptions['ssl']['capath'] = $config->get('capath');
             }
-            $remoteFilesystemOptions = array_merge_recursive($remoteFilesystemOptions, $options);
+            $remoteFilesystemOptions = array_replace_recursive($remoteFilesystemOptions, $options);
         }
         try {
             $remoteFilesystem = new RemoteFilesystem($io, $config, $remoteFilesystemOptions, $disableTls);
@@ -612,7 +625,7 @@ class Factory
     }
 
     /**
-     * @return boolean
+     * @return bool
      */
     private static function useXdg()
     {
@@ -626,8 +639,8 @@ class Factory
     }
 
     /**
-     * @return string
      * @throws \RuntimeException
+     * @return string
      */
     private static function getUserDir()
     {

+ 12 - 14
src/Composer/IO/BaseIO.php

@@ -60,27 +60,25 @@ abstract class BaseIO implements IOInterface
      */
     public function loadConfiguration(Config $config)
     {
+        $githubOauth = $config->get('github-oauth') ?: array();
+        $gitlabOauth = $config->get('gitlab-oauth') ?: array();
+        $httpBasic = $config->get('http-basic') ?: array();
+
         // reload oauth token from config if available
-        if ($tokens = $config->get('github-oauth')) {
-            foreach ($tokens as $domain => $token) {
-                if (!preg_match('{^[a-z0-9]+$}', $token)) {
-                    throw new \UnexpectedValueException('Your github oauth token for '.$domain.' contains invalid characters: "'.$token.'"');
-                }
-                $this->setAuthentication($domain, $token, 'x-oauth-basic');
+        foreach ($githubOauth as $domain => $token) {
+            if (!preg_match('{^[a-z0-9]+$}', $token)) {
+                throw new \UnexpectedValueException('Your github oauth token for '.$domain.' contains invalid characters: "'.$token.'"');
             }
+            $this->setAuthentication($domain, $token, 'x-oauth-basic');
         }
 
-        if ($tokens = $config->get('gitlab-oauth')) {
-            foreach ($tokens as $domain => $token) {
-                $this->setAuthentication($domain, $token, 'oauth2');
-            }
+        foreach ($gitlabOauth as $domain => $token) {
+            $this->setAuthentication($domain, $token, 'oauth2');
         }
 
         // reload http basic credentials from config if available
-        if ($creds = $config->get('http-basic')) {
-            foreach ($creds as $domain => $cred) {
-                $this->setAuthentication($domain, $cred['username'], $cred['password']);
-            }
+        foreach ($httpBasic as $domain => $cred) {
+            $this->setAuthentication($domain, $cred['username'], $cred['password']);
         }
 
         // setup process timeout

+ 1 - 1
src/Composer/IO/BufferIO.php

@@ -35,7 +35,7 @@ class BufferIO extends ConsoleIO
         $input = new StringInput($input);
         $input->setInteractive(false);
 
-        $output = new StreamOutput(fopen('php://memory', 'rw'), $verbosity, !empty($formatter), $formatter);
+        $output = new StreamOutput(fopen('php://memory', 'rw'), $verbosity, $formatter ? $formatter->isDecorated() : false, $formatter);
 
         parent::__construct($input, $output, new HelperSet(array()));
     }

+ 32 - 17
src/Composer/IO/ConsoleIO.php

@@ -33,6 +33,7 @@ class ConsoleIO extends BaseIO
     protected $lastMessage;
     protected $lastMessageErr;
     private $startTime;
+    private $verbosityMap;
 
     /**
      * Constructor.
@@ -46,6 +47,13 @@ class ConsoleIO extends BaseIO
         $this->input = $input;
         $this->output = $output;
         $this->helperSet = $helperSet;
+        $this->verbosityMap = array(
+            self::QUIET => OutputInterface::VERBOSITY_QUIET,
+            self::NORMAL => OutputInterface::VERBOSITY_NORMAL,
+            self::VERBOSE => OutputInterface::VERBOSITY_VERBOSE,
+            self::VERY_VERBOSE => OutputInterface::VERBOSITY_VERY_VERBOSE,
+            self::DEBUG => OutputInterface::VERBOSITY_DEBUG,
+        );
     }
 
     public function enableDebugging($startTime)
@@ -96,26 +104,32 @@ class ConsoleIO extends BaseIO
     /**
      * {@inheritDoc}
      */
-    public function write($messages, $newline = true)
+    public function write($messages, $newline = true, $verbosity = self::NORMAL)
     {
-        $this->doWrite($messages, $newline, false);
+        $this->doWrite($messages, $newline, false, $verbosity);
     }
 
     /**
      * {@inheritDoc}
      */
-    public function writeError($messages, $newline = true)
+    public function writeError($messages, $newline = true, $verbosity = self::NORMAL)
     {
-        $this->doWrite($messages, $newline, true);
+        $this->doWrite($messages, $newline, true, $verbosity);
     }
 
     /**
      * @param array|string $messages
      * @param bool         $newline
      * @param bool         $stderr
+     * @param int          $verbosity
      */
-    private function doWrite($messages, $newline, $stderr)
+    private function doWrite($messages, $newline, $stderr, $verbosity)
     {
+        $sfVerbosity = $this->verbosityMap[$verbosity];
+        if ($sfVerbosity > $this->output->getVerbosity()) {
+            return;
+        }
+
         if (null !== $this->startTime) {
             $memoryUsage = memory_get_usage() / 1024 / 1024;
             $timeSpent = microtime(true) - $this->startTime;
@@ -125,30 +139,30 @@ class ConsoleIO extends BaseIO
         }
 
         if (true === $stderr && $this->output instanceof ConsoleOutputInterface) {
-            $this->output->getErrorOutput()->write($messages, $newline);
+            $this->output->getErrorOutput()->write($messages, $newline, $sfVerbosity);
             $this->lastMessageErr = join($newline ? "\n" : '', (array) $messages);
 
             return;
         }
 
-        $this->output->write($messages, $newline);
+        $this->output->write($messages, $newline, $sfVerbosity);
         $this->lastMessage = join($newline ? "\n" : '', (array) $messages);
     }
 
     /**
      * {@inheritDoc}
      */
-    public function overwrite($messages, $newline = true, $size = null)
+    public function overwrite($messages, $newline = true, $size = null, $verbosity = self::NORMAL)
     {
-        $this->doOverwrite($messages, $newline, $size, false);
+        $this->doOverwrite($messages, $newline, $size, false, $verbosity);
     }
 
     /**
      * {@inheritDoc}
      */
-    public function overwriteError($messages, $newline = true, $size = null)
+    public function overwriteError($messages, $newline = true, $size = null, $verbosity = self::NORMAL)
     {
-        $this->doOverwrite($messages, $newline, $size, true);
+        $this->doOverwrite($messages, $newline, $size, true, $verbosity);
     }
 
     /**
@@ -156,8 +170,9 @@ class ConsoleIO extends BaseIO
      * @param bool         $newline
      * @param int|null     $size
      * @param bool         $stderr
+     * @param int          $verbosity
      */
-    private function doOverwrite($messages, $newline, $size, $stderr)
+    private function doOverwrite($messages, $newline, $size, $stderr, $verbosity)
     {
         // messages can be an array, let's convert it to string anyway
         $messages = join($newline ? "\n" : '', (array) $messages);
@@ -168,21 +183,21 @@ class ConsoleIO extends BaseIO
             $size = strlen(strip_tags($stderr ? $this->lastMessageErr : $this->lastMessage));
         }
         // ...let's fill its length with backspaces
-        $this->doWrite(str_repeat("\x08", $size), false, $stderr);
+        $this->doWrite(str_repeat("\x08", $size), false, $stderr, $verbosity);
 
         // write the new message
-        $this->doWrite($messages, false, $stderr);
+        $this->doWrite($messages, false, $stderr, $verbosity);
 
         $fill = $size - strlen(strip_tags($messages));
         if ($fill > 0) {
             // whitespace whatever has left
-            $this->doWrite(str_repeat(' ', $fill), false, $stderr);
+            $this->doWrite(str_repeat(' ', $fill), false, $stderr, $verbosity);
             // move the cursor back
-            $this->doWrite(str_repeat("\x08", $fill), false, $stderr);
+            $this->doWrite(str_repeat("\x08", $fill), false, $stderr, $verbosity);
         }
 
         if ($newline) {
-            $this->doWrite('', true, $stderr);
+            $this->doWrite('', true, $stderr, $verbosity);
         }
 
         if ($stderr) {

+ 24 - 14
src/Composer/IO/IOInterface.php

@@ -21,6 +21,12 @@ use Composer\Config;
  */
 interface IOInterface
 {
+    const QUIET = 1;
+    const NORMAL = 2;
+    const VERBOSE = 4;
+    const VERY_VERBOSE = 8;
+    const DEBUG = 16;
+
     /**
      * Is this input means interactive?
      *
@@ -59,36 +65,40 @@ interface IOInterface
     /**
      * Writes a message to the output.
      *
-     * @param string|array $messages The message as an array of lines or a single string
-     * @param bool         $newline  Whether to add a newline or not
+     * @param string|array $messages  The message as an array of lines or a single string
+     * @param bool         $newline   Whether to add a newline or not
+     * @param int          $verbosity Verbosity level from the VERBOSITY_* constants
      */
-    public function write($messages, $newline = true);
+    public function write($messages, $newline = true, $verbosity = self::NORMAL);
 
     /**
      * Writes a message to the error output.
      *
-     * @param string|array $messages The message as an array of lines or a single string
-     * @param bool         $newline  Whether to add a newline or not
+     * @param string|array $messages  The message as an array of lines or a single string
+     * @param bool         $newline   Whether to add a newline or not
+     * @param int          $verbosity Verbosity level from the VERBOSITY_* constants
      */
-    public function writeError($messages, $newline = true);
+    public function writeError($messages, $newline = true, $verbosity = self::NORMAL);
 
     /**
      * Overwrites a previous message to the output.
      *
-     * @param string|array $messages The message as an array of lines or a single string
-     * @param bool         $newline  Whether to add a newline or not
-     * @param int          $size     The size of line
+     * @param string|array $messages  The message as an array of lines or a single string
+     * @param bool         $newline   Whether to add a newline or not
+     * @param int          $size      The size of line
+     * @param int          $verbosity Verbosity level from the VERBOSITY_* constants
      */
-    public function overwrite($messages, $newline = true, $size = null);
+    public function overwrite($messages, $newline = true, $size = null, $verbosity = self::NORMAL);
 
     /**
      * Overwrites a previous message to the error output.
      *
-     * @param string|array $messages The message as an array of lines or a single string
-     * @param bool         $newline  Whether to add a newline or not
-     * @param int          $size     The size of line
+     * @param string|array $messages  The message as an array of lines or a single string
+     * @param bool         $newline   Whether to add a newline or not
+     * @param int          $size      The size of line
+     * @param int          $verbosity Verbosity level from the VERBOSITY_* constants
      */
-    public function overwriteError($messages, $newline = true, $size = null);
+    public function overwriteError($messages, $newline = true, $size = null, $verbosity = self::NORMAL);
 
     /**
      * Asks a question to the user.

+ 4 - 4
src/Composer/IO/NullIO.php

@@ -62,28 +62,28 @@ class NullIO extends BaseIO
     /**
      * {@inheritDoc}
      */
-    public function write($messages, $newline = true)
+    public function write($messages, $newline = true, $verbosity = self::NORMAL)
     {
     }
 
     /**
      * {@inheritDoc}
      */
-    public function writeError($messages, $newline = true)
+    public function writeError($messages, $newline = true, $verbosity = self::NORMAL)
     {
     }
 
     /**
      * {@inheritDoc}
      */
-    public function overwrite($messages, $newline = true, $size = 80)
+    public function overwrite($messages, $newline = true, $size = 80, $verbosity = self::NORMAL)
     {
     }
 
     /**
      * {@inheritDoc}
      */
-    public function overwriteError($messages, $newline = true, $size = 80)
+    public function overwriteError($messages, $newline = true, $size = 80, $verbosity = self::NORMAL)
     {
     }
 

+ 4 - 8
src/Composer/Installer.php

@@ -529,10 +529,8 @@ class Installer
             return max(1, $e->getCode());
         }
 
-        if ($this->io->isVerbose()) {
-            $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies");
-            $this->io->writeError("Analyzed ".$solver->getRuleSetSize()." rules to resolve dependencies");
-        }
+        $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE);
+        $this->io->writeError("Analyzed ".$solver->getRuleSetSize()." rules to resolve dependencies", true, IOInterface::VERBOSE);
 
         // force dev packages to be updated if we update or install from a (potentially new) lock
         $operations = $this->processDevPackages($localRepo, $pool, $policy, $repositories, $installedRepo, $lockedRepository, $installFromLock, $withDevReqs, 'force-updates', $operations);
@@ -578,10 +576,8 @@ class Installer
                     && (!$operation->getTargetPackage()->getSourceReference() || $operation->getTargetPackage()->getSourceReference() === $operation->getInitialPackage()->getSourceReference())
                     && (!$operation->getTargetPackage()->getDistReference() || $operation->getTargetPackage()->getDistReference() === $operation->getInitialPackage()->getDistReference())
                 ) {
-                    if ($this->io->isDebug()) {
-                        $this->io->writeError('  - Skipping update of '. $operation->getTargetPackage()->getPrettyName().' to the same reference-locked version');
-                        $this->io->writeError('');
-                    }
+                    $this->io->writeError('  - Skipping update of '. $operation->getTargetPackage()->getPrettyName().' to the same reference-locked version', true, IOInterface::DEBUG);
+                    $this->io->writeError('', true, IOInterface::DEBUG);
 
                     continue;
                 }

+ 7 - 5
src/Composer/Installer/LibraryInstaller.php

@@ -17,7 +17,9 @@ use Composer\IO\IOInterface;
 use Composer\Repository\InstalledRepositoryInterface;
 use Composer\Package\PackageInterface;
 use Composer\Util\Filesystem;
+use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
+use Composer\Util\Silencer;
 
 /**
  * Package installation manager.
@@ -130,7 +132,7 @@ class LibraryInstaller implements InstallerInterface
         if (strpos($package->getName(), '/')) {
             $packageVendorDir = dirname($downloadPath);
             if (is_dir($packageVendorDir) && $this->filesystem->isDirEmpty($packageVendorDir)) {
-                @rmdir($packageVendorDir);
+                Silencer::call('rmdir', $packageVendorDir);
             }
         }
     }
@@ -233,14 +235,14 @@ class LibraryInstaller implements InstallerInterface
                     // likely leftover from a previous install, make sure
                     // that the target is still executable in case this
                     // is a fresh install of the vendor.
-                    @chmod($link, 0777 & ~umask());
+                    Silencer::call('chmod', $link, 0777 & ~umask());
                 }
                 $this->io->writeError('    Skipped installation of bin '.$bin.' for package '.$package->getName().': name conflicts with an existing file');
                 continue;
             }
 
             if ($this->binCompat === "auto") {
-                if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+                if (Platform::isWindows()) {
                     $this->installFullBinaries($binPath, $link, $bin, $package);
                 } else {
                     $this->installSymlinkBinaries($binPath, $link);
@@ -248,7 +250,7 @@ class LibraryInstaller implements InstallerInterface
             } elseif ($this->binCompat === "full") {
                 $this->installFullBinaries($binPath, $link, $bin, $package);
             }
-            @chmod($link, 0777 & ~umask());
+            Silencer::call('chmod', $link, 0777 & ~umask());
         }
     }
 
@@ -298,7 +300,7 @@ class LibraryInstaller implements InstallerInterface
 
         // attempt removing the bin dir in case it is left empty
         if ((is_dir($this->binDir)) && ($this->filesystem->isDirEmpty($this->binDir))) {
-            @rmdir($this->binDir);
+            Silencer::call('rmdir', $this->binDir);
         }
     }
 

+ 3 - 4
src/Composer/Installer/PearInstaller.php

@@ -17,6 +17,7 @@ use Composer\Composer;
 use Composer\Downloader\PearPackageExtractor;
 use Composer\Repository\InstalledRepositoryInterface;
 use Composer\Package\PackageInterface;
+use Composer\Util\Platform;
 use Composer\Util\ProcessExecutor;
 
 /**
@@ -53,7 +54,7 @@ class PearInstaller extends LibraryInstaller
         parent::installCode($package);
         parent::initializeBinDir();
 
-        $isWindows = defined('PHP_WINDOWS_VERSION_BUILD');
+        $isWindows = Platform::isWindows();
         $php_bin = $this->binDir . ($isWindows ? '/composer-php.bat' : '/composer-php');
 
         if (!$isWindows) {
@@ -75,9 +76,7 @@ class PearInstaller extends LibraryInstaller
         $pearExtractor = new PearPackageExtractor($packageArchive);
         $pearExtractor->extractTo($this->getInstallPath($package), array('php' => '/', 'script' => '/bin', 'data' => '/data'), $vars);
 
-        if ($this->io->isVerbose()) {
-            $this->io->writeError('    Cleaning up');
-        }
+        $this->io->writeError('    Cleaning up', true, IOInterface::VERBOSE);
         $this->filesystem->unlink($packageArchive);
     }
 

+ 1 - 1
src/Composer/Package/AliasPackage.php

@@ -165,7 +165,7 @@ class AliasPackage extends BasePackage implements CompletePackageInterface
     }
 
     /**
-     * @param Link[]  $links
+     * @param Link[] $links
      * @param string $linkType
      *
      * @return Link[]

+ 5 - 0
src/Composer/Package/Loader/RootPackageLoader.php

@@ -113,6 +113,11 @@ class RootPackageLoader extends ArrayLoader
             }
         }
 
+        if (isset($links[$config['name']])) {
+            throw new \InvalidArgumentException(sprintf('Root package \'%s\' cannot require itself in its composer.json' . PHP_EOL .
+                        'Did you accidentally name your root package after an external package?', $config['name']));
+        }
+
         $realPackage->setAliases($aliases);
         $realPackage->setStabilityFlags($stabilityFlags);
         $realPackage->setReferences($references);

+ 23 - 0
src/Composer/Plugin/Capability/Capability.php

@@ -0,0 +1,23 @@
+<?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\Plugin\Capability;
+
+/**
+ * Marker interface for Plugin capabilities.
+ * Every new Capability which is added to the Plugin API must implement this interface.
+ *
+ * @api
+ */
+interface Capability
+{
+}

+ 43 - 0
src/Composer/Plugin/Capable.php

@@ -0,0 +1,43 @@
+<?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\Plugin;
+
+/**
+ * Plugins which need to expose various implementations
+ * of the Composer Plugin Capabilities must have their
+ * declared Plugin class implementing this interface.
+ *
+ * @api
+ */
+interface Capable
+{
+    /**
+     * Method by which a Plugin announces its API implementations, through an array
+     * with a special structure.
+     *
+     * The key must be a string, representing a fully qualified class/interface name
+     * which Composer Plugin API exposes.
+     * The value must be a string as well, representing the fully qualified class name
+     * of the implementing class.
+     *
+     * @tutorial
+     *
+     * return array(
+     *     'Composer\Plugin\Capability\CommandProvider' => 'My\CommandProvider',
+     *     'Composer\Plugin\Capability\Validator'       => 'My\Validator',
+     * );
+     *
+     * @return string[]
+     */
+    public function getCapabilities();
+}

+ 2 - 2
src/Composer/Plugin/PluginInterface.php

@@ -23,14 +23,14 @@ use Composer\IO\IOInterface;
 interface PluginInterface
 {
     /**
-     * Version number of the fake composer-plugin-api package
+     * Version number of the internal composer-plugin-api package
      *
      * @var string
      */
     const PLUGIN_API_VERSION = '1.0.0';
 
     /**
-     * Apply plugin modifications to composer
+     * Apply plugin modifications to Composer
      *
      * @param Composer    $composer
      * @param IOInterface $io

+ 60 - 4
src/Composer/Plugin/PluginManager.php

@@ -23,6 +23,7 @@ use Composer\Package\PackageInterface;
 use Composer\Package\Link;
 use Composer\Semver\Constraint\Constraint;
 use Composer\DependencyResolver\Pool;
+use Composer\Plugin\Capability\Capability;
 
 /**
  * Plugin manager
@@ -122,8 +123,11 @@ class PluginManager
             $currentPluginApiVersion = $this->getPluginApiVersion();
             $currentPluginApiConstraint = new Constraint('==', $this->versionParser->normalize($currentPluginApiVersion));
 
-            if (!$requiresComposer->matches($currentPluginApiConstraint)) {
+            if ($requiresComposer->getPrettyString() === '1.0.0' && $this->getPluginApiVersion() === '1.0.0') {
+                $this->io->writeError('<warning>The "' . $package->getName() . '" plugin requires composer-plugin-api 1.0.0, this *WILL* break in the future and it should be fixed ASAP (require ^1.0 for example).</warning>');
+            } elseif (!$requiresComposer->matches($currentPluginApiConstraint)) {
                 $this->io->writeError('<warning>The "' . $package->getName() . '" plugin was skipped because it requires a Plugin API version ("' . $requiresComposer->getPrettyString() . '") that does not match your Composer installation ("' . $currentPluginApiVersion . '"). You may need to run composer update with the "--no-plugins" option.</warning>');
+
                 return;
             }
         }
@@ -202,9 +206,7 @@ class PluginManager
      */
     private function addPlugin(PluginInterface $plugin)
     {
-        if ($this->io->isDebug()) {
-            $this->io->writeError('Loading plugin '.get_class($plugin));
-        }
+        $this->io->writeError('Loading plugin '.get_class($plugin), true, IOInterface::DEBUG);
         $this->plugins[] =  $plugin;
         $plugin->activate($this->composer, $this->io);
 
@@ -299,4 +301,58 @@ class PluginManager
 
         return $this->globalComposer->getInstallationManager()->getInstallPath($package);
     }
+
+    /**
+     * @param  PluginInterface   $plugin
+     * @param  string            $capability
+     * @throws \RuntimeException On empty or non-string implementation class name value
+     * @return null|string       The fully qualified class of the implementation or null if Plugin is not of Capable type or does not provide it
+     */
+    protected function getCapabilityImplementationClassName(PluginInterface $plugin, $capability)
+    {
+        if (!($plugin instanceof Capable)) {
+            return null;
+        }
+
+        $capabilities = (array) $plugin->getCapabilities();
+
+        if (!empty($capabilities[$capability]) && is_string($capabilities[$capability]) && trim($capabilities[$capability])) {
+            return trim($capabilities[$capability]);
+        }
+
+        if (
+            array_key_exists($capability, $capabilities)
+            && (empty($capabilities[$capability]) || !is_string($capabilities[$capability]) || !trim($capabilities[$capability]))
+        ) {
+            throw new \UnexpectedValueException('Plugin '.get_class($plugin).' provided invalid capability class name(s), got '.var_export($capabilities[$capability], 1));
+        }
+    }
+
+    /**
+     * @param  PluginInterface $plugin
+     * @param  string          $capabilityClassName The fully qualified name of the API interface which the plugin may provide
+     *                                              an implementation of.
+     * @param  array           $ctorArgs            Arguments passed to Capability's constructor.
+     *                                              Keeping it an array will allow future values to be passed w\o changing the signature.
+     * @return null|Capability
+     */
+    public function getPluginCapability(PluginInterface $plugin, $capabilityClassName, array $ctorArgs = array())
+    {
+        if ($capabilityClass = $this->getCapabilityImplementationClassName($plugin, $capabilityClassName)) {
+            if (!class_exists($capabilityClass)) {
+                throw new \RuntimeException("Cannot instantiate Capability, as class $capabilityClass from plugin ".get_class($plugin)." does not exist.");
+            }
+
+            $capabilityObj = new $capabilityClass($ctorArgs);
+
+            // FIXME these could use is_a and do the check *before* instantiating once drop support for php<5.3.9
+            if (!$capabilityObj instanceof Capability || !$capabilityObj instanceof $capabilityClassName) {
+                throw new \RuntimeException(
+                    'Class ' . $capabilityClass . ' must implement both Composer\Plugin\Capability\Capability and '. $capabilityClassName . '.'
+                );
+            }
+
+            return $capabilityObj;
+        }
+    }
 }

+ 3 - 7
src/Composer/Repository/ArtifactRepository.php

@@ -67,16 +67,12 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito
 
             $package = $this->getComposerInformation($file);
             if (!$package) {
-                if ($io->isVerbose()) {
-                    $io->writeError("File <comment>{$file->getBasename()}</comment> doesn't seem to hold a package");
-                }
+                $io->writeError("File <comment>{$file->getBasename()}</comment> doesn't seem to hold a package", true, IOInterface::VERBOSE);
                 continue;
             }
 
-            if ($io->isVerbose()) {
-                $template = 'Found package <info>%s</info> (<comment>%s</comment>) in file <info>%s</info>';
-                $io->writeError(sprintf($template, $package->getName(), $package->getPrettyVersion(), $file->getBasename()));
-            }
+            $template = 'Found package <info>%s</info> (<comment>%s</comment>) in file <info>%s</info>';
+            $io->writeError(sprintf($template, $package->getName(), $package->getPrettyVersion(), $file->getBasename()), true, IOInterface::VERBOSE);
 
             $this->addPackage($package);
         }

+ 1 - 0
src/Composer/Repository/ComposerRepository.php

@@ -747,6 +747,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
                     $this->io->writeError('<warning>'.$this->url.' could not be fully loaded, package information was loaded from the local cache and may be out of date</warning>');
                 }
                 $this->degradedMode = true;
+
                 return true;
             }
         }

+ 9 - 6
src/Composer/Repository/PathRepository.php

@@ -113,7 +113,7 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn
         parent::initialize();
 
         foreach ($this->getUrlMatches() as $url) {
-            $path = realpath($url) . '/';
+            $path = realpath($url) . DIRECTORY_SEPARATOR;
             $composerFilePath = $path.'composer.json';
 
             if (!file_exists($composerFilePath)) {
@@ -125,16 +125,16 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn
             $package['dist'] = array(
                 'type' => 'path',
                 'url' => $url,
-                'reference' => '',
+                'reference' => sha1($json),
             );
 
             if (!isset($package['version'])) {
                 $package['version'] = $this->versionGuesser->guessVersion($package, $path) ?: 'dev-master';
             }
-            if (is_dir($path.'/.git') && 0 === $this->process->execute('git log -n1 --pretty=%H', $output, $path)) {
+
+            $output = '';
+            if (is_dir($path . DIRECTORY_SEPARATOR . '.git') && 0 === $this->process->execute('git log -n1 --pretty=%H', $output, $path)) {
                 $package['dist']['reference'] = trim($output);
-            } else {
-                $package['dist']['reference'] = Locker::getContentHash($json);
             }
 
             $package = $this->loader->load($package);
@@ -153,6 +153,9 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn
      */
     private function getUrlMatches()
     {
-        return glob($this->url, GLOB_MARK | GLOB_ONLYDIR);
+        // Ensure environment-specific path separators are normalized to URL separators
+        return array_map(function ($val) {
+            return str_replace(DIRECTORY_SEPARATOR, '/', $val);
+        }, glob($this->url, GLOB_MARK | GLOB_ONLYDIR));
     }
 }

+ 1 - 3
src/Composer/Repository/PearRepository.php

@@ -105,9 +105,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn
                 try {
                     $normalizedVersion = $versionParser->normalize($version);
                 } catch (\UnexpectedValueException $e) {
-                    if ($this->io->isVerbose()) {
-                        $this->io->writeError('Could not load '.$packageDefinition->getPackageName().' '.$version.': '.$e->getMessage());
-                    }
+                    $this->io->writeError('Could not load '.$packageDefinition->getPackageName().' '.$version.': '.$e->getMessage(), true, IOInterface::VERBOSE);
                     continue;
                 }
 

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

@@ -203,6 +203,7 @@ class PlatformRepository extends ArrayRepository
         if (isset($this->overrides[strtolower($package->getName())])) {
             $overrider = $this->findPackage($package->getName(), '*');
             $overrider->setDescription($overrider->getDescription().' (actual: '.$package->getPrettyVersion().')');
+
             return;
         }
         parent::addPackage($package);

+ 0 - 1
src/Composer/Repository/RepositoryManager.php

@@ -105,7 +105,6 @@ class RepositoryManager
 
         $class = $this->repositoryClasses[$type];
 
-
         $reflMethod = new \ReflectionMethod($class, '__construct');
         $params = $reflMethod->getParameters();
         if (isset($params[4]) && $params[4]->getClass() && $params[4]->getClass()->getName() === 'Composer\Util\RemoteFilesystem') {

+ 1 - 3
src/Composer/Repository/Vcs/GitBitbucketDriver.php

@@ -160,9 +160,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
         }
 
         if (!extension_loaded('openssl')) {
-            if ($io->isVerbose()) {
-                $io->writeError('Skipping Bitbucket git driver for '.$url.' because the OpenSSL PHP extension is missing.');
-            }
+            $io->writeError('Skipping Bitbucket git driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE);
 
             return false;
         }

+ 1 - 3
src/Composer/Repository/Vcs/GitHubDriver.php

@@ -268,9 +268,7 @@ class GitHubDriver extends VcsDriver
         }
 
         if (!extension_loaded('openssl')) {
-            if ($io->isVerbose()) {
-                $io->writeError('Skipping GitHub driver for '.$url.' because the OpenSSL PHP extension is missing.');
-            }
+            $io->writeError('Skipping GitHub driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE);
 
             return false;
         }

+ 1 - 3
src/Composer/Repository/Vcs/GitLabDriver.php

@@ -367,9 +367,7 @@ class GitLabDriver extends VcsDriver
         }
 
         if ('https' === $scheme && !extension_loaded('openssl')) {
-            if ($io->isVerbose()) {
-                $io->write('Skipping GitLab driver for '.$url.' because the OpenSSL PHP extension is missing.');
-            }
+            $io->writeError('Skipping GitLab driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE);
 
             return false;
         }

+ 1 - 3
src/Composer/Repository/Vcs/HgBitbucketDriver.php

@@ -170,9 +170,7 @@ class HgBitbucketDriver extends VcsDriver
         }
 
         if (!extension_loaded('openssl')) {
-            if ($io->isVerbose()) {
-                $io->writeError('Skipping Bitbucket hg driver for '.$url.' because the OpenSSL PHP extension is missing.');
-            }
+            $io->writeError('Skipping Bitbucket hg driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE);
 
             return false;
         }

+ 0 - 1
src/Composer/Util/ConfigValidator.php

@@ -19,7 +19,6 @@ use Composer\Json\JsonValidationException;
 use Composer\IO\IOInterface;
 use Composer\Json\JsonFile;
 use Composer\Spdx\SpdxLicenses;
-use Composer\Factory;
 
 /**
  * Validates a composer configuration.

+ 3 - 2
src/Composer/Util/ErrorHandler.php

@@ -36,8 +36,8 @@ class ErrorHandler
      */
     public static function handle($level, $message, $file, $line)
     {
-        // respect error_reporting being disabled
-        if (!error_reporting()) {
+        // error code is not included in error_reporting
+        if (!(error_reporting() & $level)) {
             return;
         }
 
@@ -73,6 +73,7 @@ class ErrorHandler
     public static function register(IOInterface $io = null)
     {
         set_error_handler(array(__CLASS__, 'handle'));
+        error_reporting(E_ALL | E_STRICT);
         self::$io = $io;
     }
 }

+ 8 - 8
src/Composer/Util/Filesystem.php

@@ -110,7 +110,7 @@ class Filesystem
             return $this->removeDirectoryPhp($directory);
         }
 
-        if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+        if (Platform::isWindows()) {
             $cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape(realpath($directory)));
         } else {
             $cmd = sprintf('rm -rf %s', ProcessExecutor::escape($directory));
@@ -181,10 +181,10 @@ class Filesystem
     {
         if (!@$this->unlinkImplementation($path)) {
             // retry after a bit on windows since it tends to be touchy with mass removals
-            if (!defined('PHP_WINDOWS_VERSION_BUILD') || (usleep(350000) && !@$this->unlinkImplementation($path))) {
+            if (!Platform::isWindows() || (usleep(350000) && !@$this->unlinkImplementation($path))) {
                 $error = error_get_last();
                 $message = 'Could not delete '.$path.': ' . @$error['message'];
-                if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+                if (Platform::isWindows()) {
                     $message .= "\nThis can be due to an antivirus or the Windows Search Indexer locking the file while they are analyzed";
                 }
 
@@ -206,10 +206,10 @@ class Filesystem
     {
         if (!@rmdir($path)) {
             // retry after a bit on windows since it tends to be touchy with mass removals
-            if (!defined('PHP_WINDOWS_VERSION_BUILD') || (usleep(350000) && !@rmdir($path))) {
+            if (!Platform::isWindows() || (usleep(350000) && !@rmdir($path))) {
                 $error = error_get_last();
                 $message = 'Could not delete '.$path.': ' . @$error['message'];
-                if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+                if (Platform::isWindows()) {
                     $message .= "\nThis can be due to an antivirus or the Windows Search Indexer locking the file while they are analyzed";
                 }
 
@@ -264,7 +264,7 @@ class Filesystem
             return $this->copyThenRemove($source, $target);
         }
 
-        if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+        if (Platform::isWindows()) {
             // Try to copy & delete - this is a workaround for random "Access denied" errors.
             $command = sprintf('xcopy %s %s /E /I /Q /Y', ProcessExecutor::escape($source), ProcessExecutor::escape($target));
             $result = $this->processExecutor->execute($command, $output);
@@ -460,7 +460,7 @@ class Filesystem
 
     public static function getPlatformPath($path)
     {
-        if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+        if (Platform::isWindows()) {
             $path = preg_replace('{^(?:file:///([a-z])/)}i', 'file://$1:/', $path);
         }
 
@@ -498,7 +498,7 @@ class Filesystem
      */
     private function unlinkImplementation($path)
     {
-        if (defined('PHP_WINDOWS_VERSION_BUILD') && is_dir($path) && is_link($path)) {
+        if (Platform::isWindows() && is_dir($path) && is_link($path)) {
             return rmdir($path);
         }
 

+ 36 - 0
src/Composer/Util/Keys.php

@@ -0,0 +1,36 @@
+<?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;
+
+/**
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class Keys
+{
+    public static function fingerprint($path)
+    {
+        $hash = strtoupper(hash('sha256', preg_replace('{\s}', '', file_get_contents($path))));
+
+        return implode(' ', array(
+            substr($hash, 0, 8),
+            substr($hash, 8, 8),
+            substr($hash, 16, 8),
+            substr($hash, 24, 8),
+            '', // Extra space
+            substr($hash, 32, 8),
+            substr($hash, 40, 8),
+            substr($hash, 48, 8),
+            substr($hash, 56, 8),
+        ));
+    }
+}

+ 1 - 4
src/Composer/Util/Perforce.php

@@ -51,10 +51,7 @@ class Perforce
 
     public static function create($repoConfig, $port, $path, ProcessExecutor $process, IOInterface $io)
     {
-        $isWindows = defined('PHP_WINDOWS_VERSION_BUILD');
-        $perforce = new Perforce($repoConfig, $port, $path, $process, $isWindows, $io);
-
-        return $perforce;
+        return new Perforce($repoConfig, $port, $path, $process, Platform::isWindows(), $io);
     }
 
     public static function checkServerExists($url, ProcessExecutor $processExecutor)

+ 28 - 0
src/Composer/Util/Platform.php

@@ -0,0 +1,28 @@
+<?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;
+
+/**
+ * Platform helper for uniform platform-specific tests.
+ *
+ * @author Niels Keurentjes <niels.keurentjes@omines.com>
+ */
+class Platform
+{
+    /**
+     * @return bool Whether the host machine is running a Windows OS
+     */
+    public static function isWindows()
+    {
+        return defined('PHP_WINDOWS_VERSION_BUILD');
+    }
+}

+ 1 - 1
src/Composer/Util/ProcessExecutor.php

@@ -50,7 +50,7 @@ class ProcessExecutor
 
         // make sure that null translate to the proper directory in case the dir is a symlink
         // and we call a git command, because msysgit does not handle symlinks properly
-        if (null === $cwd && defined('PHP_WINDOWS_VERSION_BUILD') && false !== strpos($command, 'git') && getcwd()) {
+        if (null === $cwd && Platform::isWindows() && false !== strpos($command, 'git') && getcwd()) {
             $cwd = realpath(getcwd());
         }
 

+ 345 - 108
src/Composer/Util/RemoteFilesystem.php

@@ -33,11 +33,14 @@ class RemoteFilesystem
     private $progress;
     private $lastProgress;
     private $options = array();
+    private $peerCertificateMap = array();
     private $disableTls = false;
     private $retryAuthFailure;
     private $lastHeaders;
     private $storeAuth;
     private $degradedMode = false;
+    private $redirects;
+    private $maxRedirects = 20;
 
     /**
      * Constructor.
@@ -54,15 +57,7 @@ class RemoteFilesystem
         // Setup TLS options
         // The cafile option can be set via config.json
         if ($disableTls === false) {
-            $this->options = $this->getTlsDefaults();
-            if (isset($options['ssl']['cafile'])
-                && (
-                    !is_readable($options['ssl']['cafile'])
-                    || !$this->validateCaFile($options['ssl']['cafile'])
-                )
-            ) {
-                throw new TransportException('The configured cafile was not valid or could not be read.');
-            }
+            $this->options = $this->getTlsDefaults($options);
         } else {
             $this->disableTls = true;
         }
@@ -139,8 +134,8 @@ class RemoteFilesystem
     }
 
     /**
-     * @param array $headers array of returned headers like from getLastHeaders()
-     * @param string $name header name (case insensitive)
+     * @param  array       $headers array of returned headers like from getLastHeaders()
+     * @param  string      $name    header name (case insensitive)
      * @return string|null
      */
     public function findHeaderValue(array $headers, $name)
@@ -160,7 +155,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
      */
     public function findStatusCode(array $headers)
@@ -206,24 +201,34 @@ class RemoteFilesystem
         $this->lastProgress = null;
         $this->retryAuthFailure = true;
         $this->lastHeaders = array();
+        $this->redirects = 1; // The first request counts.
 
         // capture username/password from URL if there is one
         if (preg_match('{^https?://(.+):(.+)@([^/]+)}i', $fileUrl, $match)) {
             $this->io->setAuthentication($originUrl, urldecode($match[1]), urldecode($match[2]));
         }
 
-        if (isset($additionalOptions['retry-auth-failure'])) {
-            $this->retryAuthFailure = (bool) $additionalOptions['retry-auth-failure'];
+        $tempAdditionalOptions = $additionalOptions;
+        if (isset($tempAdditionalOptions['retry-auth-failure'])) {
+            $this->retryAuthFailure = (bool) $tempAdditionalOptions['retry-auth-failure'];
 
-            unset($additionalOptions['retry-auth-failure']);
+            unset($tempAdditionalOptions['retry-auth-failure']);
         }
 
-        $options = $this->getOptionsForUrl($originUrl, $additionalOptions);
+        $isRedirect = false;
+        if (isset($tempAdditionalOptions['redirects'])) {
+            $this->redirects = $tempAdditionalOptions['redirects'];
+            $isRedirect = true;
 
-        if ($this->io->isDebug()) {
-            $this->io->writeError((substr($fileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $fileUrl);
+            unset($tempAdditionalOptions['redirects']);
         }
 
+        $options = $this->getOptionsForUrl($originUrl, $tempAdditionalOptions);
+        unset($tempAdditionalOptions);
+        $userlandFollow = isset($options['http']['follow_location']) && !$options['http']['follow_location'];
+
+        $this->io->writeError((substr($fileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $fileUrl, true, IOInterface::DEBUG);
+
         if (isset($options['github-token'])) {
             $fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['github-token'];
             unset($options['github-token']);
@@ -245,7 +250,7 @@ class RemoteFilesystem
 
         $ctx = StreamContextFactory::getContext($fileUrl, $options, array('notification' => array($this, 'callbackGet')));
 
-        if ($this->progress) {
+        if ($this->progress && !$isRedirect) {
             $this->io->writeError("    Downloading: <comment>Connecting...</comment>", false);
         }
 
@@ -260,6 +265,18 @@ class RemoteFilesystem
         });
         try {
             $result = file_get_contents($fileUrl, false, $ctx);
+
+            if (PHP_VERSION_ID < 50600 && !empty($options['ssl']['peer_fingerprint'])) {
+                // Emulate fingerprint validation on PHP < 5.6
+                $params = stream_context_get_params($ctx);
+                $expectedPeerFingerprint = $options['ssl']['peer_fingerprint'];
+                $peerFingerprint = TlsHelper::getCertificateFingerprint($params['options']['ssl']['peer_certificate']);
+
+                // Constant time compare??!
+                if ($expectedPeerFingerprint !== $peerFingerprint) {
+                    throw new TransportException('Peer fingerprint did not match');
+                }
+            }
         } catch (\Exception $e) {
             if ($e instanceof TransportException && !empty($http_response_header[0])) {
                 $e->setHeaders($http_response_header);
@@ -293,6 +310,11 @@ class RemoteFilesystem
             $statusCode = $this->findStatusCode($http_response_header);
         }
 
+        // handle 3xx redirects for php<5.6, 304 Not Modified is excluded
+        if ($userlandFollow && $statusCode >= 300 && $statusCode <= 399 && $statusCode !== 304 && $this->redirects < $this->maxRedirects) {
+            $result = $this->handleRedirect($http_response_header, $additionalOptions, $result);
+        }
+
         // fail 4xx and 5xx responses and capture the response
         if ($statusCode && $statusCode >= 400 && $statusCode <= 599) {
             if (!$this->retry) {
@@ -305,7 +327,7 @@ class RemoteFilesystem
             $result = false;
         }
 
-        if ($this->progress && !$this->retry) {
+        if ($this->progress && !$this->retry && !$isRedirect) {
             $this->io->overwriteError("    Downloading: <comment>100%</comment>");
         }
 
@@ -342,7 +364,7 @@ class RemoteFilesystem
         }
 
         // handle copy command if download was successful
-        if (false !== $result && null !== $fileName) {
+        if (false !== $result && null !== $fileName && !$isRedirect) {
             if ('' === $result) {
                 throw new TransportException('"'.$this->fileUrl.'" appears broken, and returned an empty 200 response');
             }
@@ -361,14 +383,50 @@ class RemoteFilesystem
             }
         }
 
+        // Handle SSL cert match issues
+        if (false === $result && false !== strpos($errorMessage, 'Peer certificate') && PHP_VERSION_ID < 50600) {
+            // Certificate name error, PHP doesn't support subjectAltName on PHP < 5.6
+            // The procedure to handle sAN for older PHP's is:
+            //
+            // 1. Open socket to remote server and fetch certificate (disabling peer
+            //    validation because PHP errors without giving up the certificate.)
+            //
+            // 2. Verifying the domain in the URL against the names in the sAN field.
+            //    If there is a match record the authority [host/port], certificate
+            //    common name, and certificate fingerprint.
+            //
+            // 3. Retry the original request but changing the CN_match parameter to
+            //    the common name extracted from the certificate in step 2.
+            //
+            // 4. To prevent any attempt at being hoodwinked by switching the
+            //    certificate between steps 2 and 3 the fingerprint of the certificate
+            //    presented in step 3 is compared against the one recorded in step 2.
+            if (TlsHelper::isOpensslParseSafe()) {
+                $certDetails = $this->getCertificateCnAndFp($this->fileUrl, $options);
+
+                if ($certDetails) {
+                    $this->peerCertificateMap[$this->getUrlAuthority($this->fileUrl)] = $certDetails;
+
+                    $this->retry = true;
+                }
+            } else {
+                $this->io->writeError(sprintf(
+                    '<error>Your version of PHP, %s, is affected by CVE-2013-6420 and cannot safely perform certificate validation, we strongly suggest you upgrade.</error>',
+                    PHP_VERSION
+                ));
+            }
+        }
+
         if ($this->retry) {
             $this->retry = false;
 
             $result = $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress);
 
-            $authHelper = new AuthHelper($this->io, $this->config);
-            $authHelper->storeAuth($this->originUrl, $this->storeAuth);
-            $this->storeAuth = false;
+            if ($this->storeAuth && $this->config) {
+                $authHelper = new AuthHelper($this->io, $this->config);
+                $authHelper->storeAuth($this->originUrl, $this->storeAuth);
+                $this->storeAuth = false;
+            }
 
             return $result;
         }
@@ -522,19 +580,42 @@ class RemoteFilesystem
         $tlsOptions = array();
 
         // Setup remaining TLS options - the matching may need monitoring, esp. www vs none in CN
-        if ($this->disableTls === false && PHP_VERSION_ID < 50600) {
-            if (!preg_match('{^https?://}', $this->fileUrl)) {
-                $host = $originUrl;
+        if ($this->disableTls === false && PHP_VERSION_ID < 50600 && !stream_is_local($this->fileUrl)) {
+            $host = parse_url($this->fileUrl, PHP_URL_HOST);
+
+            if (PHP_VERSION_ID >= 50304) {
+                // Must manually follow when setting CN_match because this causes all
+                // redirects to be validated against the same CN_match value.
+                $userlandFollow = true;
             } else {
-                $host = parse_url($this->fileUrl, PHP_URL_HOST);
-            }
+                // PHP < 5.3.4 does not support follow_location, for those people
+                // do some really nasty hard coded transformations. These will
+                // still breakdown if the site redirects to a domain we don't
+                // expect.
 
-            if ($host === 'github.com' || $host === 'api.github.com') {
-                $host = '*.github.com';
+                if ($host === 'github.com' || $host === 'api.github.com') {
+                    $host = '*.github.com';
+                }
             }
 
             $tlsOptions['ssl']['CN_match'] = $host;
             $tlsOptions['ssl']['SNI_server_name'] = $host;
+
+            $urlAuthority = $this->getUrlAuthority($this->fileUrl);
+
+            if (isset($this->peerCertificateMap[$urlAuthority])) {
+                // Handle subjectAltName on lesser PHP's.
+                $certMap = $this->peerCertificateMap[$urlAuthority];
+
+                $this->io->writeError(sprintf(
+                    'Using <info>%s</info> as CN for subjectAltName enabled host <info>%s</info>',
+                    $certMap['cn'],
+                    $urlAuthority
+                ), true, IOInterface::DEBUG);
+
+                $tlsOptions['ssl']['CN_match'] = $certMap['cn'];
+                $tlsOptions['ssl']['peer_fingerprint'] = $certMap['fp'];
+            }
         }
 
         $headers = array();
@@ -551,6 +632,10 @@ class RemoteFilesystem
             $headers[] = 'Connection: close';
         }
 
+        if (isset($userlandFollow)) {
+            $options['http']['follow_location'] = 0;
+        }
+
         if ($this->io->hasAuthentication($originUrl)) {
             $auth = $this->io->getAuthentication($originUrl);
             if ('github.com' === $originUrl && 'x-oauth-basic' === $auth['password']) {
@@ -575,7 +660,55 @@ class RemoteFilesystem
         return $options;
     }
 
-    private function getTlsDefaults()
+    private function handleRedirect(array $http_response_header, array $additionalOptions, $result)
+    {
+        if ($locationHeader = $this->findHeaderValue($http_response_header, '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 = $this->scheme.':'.$locationHeader;
+            } elseif ('/' === $locationHeader[0]) {
+                // Absolute path; e.g. /foo
+                $urlHost = parse_url($this->fileUrl, PHP_URL_HOST);
+
+                // Replace path using hostname as an anchor.
+                $targetUrl = preg_replace('{^(.+(?://|@)'.preg_quote($urlHost).'(?::\d+)?)(?:[/\?].*)?$}', '\1'.$locationHeader, $this->fileUrl);
+            } else {
+                // Relative path; e.g. foo
+                // This actually differs from PHP which seems to add duplicate slashes.
+                $targetUrl = preg_replace('{^(.+/)[^/?]*(?:\?.*)?$}', '\1'.$locationHeader, $this->fileUrl);
+            }
+        }
+
+        if (!empty($targetUrl)) {
+            $this->redirects++;
+
+            $this->io->writeError(sprintf('Following redirect (%u) %s', $this->redirects, $targetUrl), true, IOInterface::DEBUG);
+
+            $additionalOptions['redirects'] = $this->redirects;
+
+            return $this->get($this->originUrl, $targetUrl, $additionalOptions, $this->fileName, $this->progress);
+        }
+
+        if (!$this->retry) {
+            $e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded, got redirect without Location ('.$http_response_header[0].')');
+            $e->setHeaders($http_response_header);
+            $e->setResponse($result);
+
+            throw $e;
+        }
+
+        return false;
+    }
+
+    /**
+     * @param array $options
+     *
+     * @return array
+     */
+    private function getTlsDefaults(array $options)
     {
         $ciphers = implode(':', array(
             'ECDHE-RSA-AES128-GCM-SHA256',
@@ -600,7 +733,7 @@ class RemoteFilesystem
             'DHE-DSS-AES256-SHA',
             'DHE-RSA-AES256-SHA',
             'AES128-GCM-SHA256',
-             'AES256-GCM-SHA384',
+            'AES256-GCM-SHA384',
             'ECDHE-RSA-RC4-SHA',
             'ECDHE-ECDSA-RC4-SHA',
             'AES128',
@@ -613,7 +746,7 @@ class RemoteFilesystem
             '!DES',
             '!3DES',
             '!MD5',
-            '!PSK'
+            '!PSK',
         ));
 
         /**
@@ -622,89 +755,96 @@ class RemoteFilesystem
          *
          * cafile or capath can be overridden by passing in those options to constructor.
          */
-        $options = array(
+        $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($this->options['ssl']['cafile'])) {
+        if (!isset($defaults['ssl']['cafile']) && !isset($defaults['ssl']['capath'])) {
             $result = $this->getSystemCaRootBundlePath();
-            if ($result) {
-                if (preg_match('{^phar://}', $result)) {
-                    $targetPath = rtrim(sys_get_temp_dir(), '\\/') . '/composer-cacert.pem';
-
-                    // use stream_copy_to_stream instead of copy
-                    // to work around https://bugs.php.net/bug.php?id=64634
-                    $source = fopen($result, 'r');
-                    $target = fopen($targetPath, 'w+');
-                    stream_copy_to_stream($source, $target);
-                    fclose($source);
-                    fclose($target);
-                    unset($source, $target);
-
-                    $options['ssl']['cafile'] = $targetPath;
-                } else {
-                    if (is_dir($result)) {
-                        $options['ssl']['capath'] = $result;
-                    } elseif ($result) {
-                        $options['ssl']['cafile'] = $result;
-                    }
+
+            if (preg_match('{^phar://}', $result)) {
+                $hash = hash_file('sha256', $result);
+                $targetPath = rtrim(sys_get_temp_dir(), '\\/') . '/composer-cacert-' . $hash . '.pem';
+
+                if (!file_exists($targetPath) || $hash !== hash_file('sha256', $targetPath)) {
+                    $this->streamCopy($result, $targetPath);
+                    chmod($targetPath, 0666);
                 }
+
+                $defaults['ssl']['cafile'] = $targetPath;
+            } elseif (is_dir($result)) {
+                $defaults['ssl']['capath'] = $result;
             } else {
-                throw new TransportException('A valid cafile could not be located automatically.');
+                $defaults['ssl']['cafile'] = $result;
             }
         }
 
+        if (isset($defaults['ssl']['cafile']) && (!is_readable($defaults['ssl']['cafile']) || !$this->validateCaFile($defaults['ssl']['cafile']))) {
+            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) {
-            $options['ssl']['disable_compression'] = true;
+            $defaults['ssl']['disable_compression'] = true;
         }
 
-        return $options;
+        return $defaults;
     }
 
     /**
-    * This method was adapted from Sslurp.
-    * https://github.com/EvanDotPro/Sslurp
-    *
-    * (c) Evan Coury <me@evancoury.com>
-    *
-    * For the full copyright and license information, please see below:
-    *
-    * Copyright (c) 2013, Evan Coury
-    * All rights reserved.
-    *
-    * Redistribution and use in source and binary forms, with or without modification,
-    * are permitted provided that the following conditions are met:
-    *
-    *     * Redistributions of source code must retain the above copyright notice,
-    *       this list of conditions and the following disclaimer.
-    *
-    *     * Redistributions in binary form must reproduce the above copyright notice,
-    *       this list of conditions and the following disclaimer in the documentation
-    *       and/or other materials provided with the distribution.
-    *
-    * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-    * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-    * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-    * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
-    * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-    * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-    * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
-    * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-    * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-    * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-    */
+     * This method was adapted from Sslurp.
+     * https://github.com/EvanDotPro/Sslurp
+     *
+     * (c) Evan Coury <me@evancoury.com>
+     *
+     * For the full copyright and license information, please see below:
+     *
+     * Copyright (c) 2013, Evan Coury
+     * All rights reserved.
+     *
+     * Redistribution and use in source and binary forms, with or without modification,
+     * are permitted provided that the following conditions are met:
+     *
+     *     * Redistributions of source code must retain the above copyright notice,
+     *       this list of conditions and the following disclaimer.
+     *
+     *     * Redistributions in binary form must reproduce the above copyright notice,
+     *       this list of conditions and the following disclaimer in the documentation
+     *       and/or other materials provided with the distribution.
+     *
+     * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+     * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+     * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+     * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+     * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+     * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+     * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+     * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+     * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+     * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+     *
+     * @return string
+     */
     private function getSystemCaRootBundlePath()
     {
         static $caPath = null;
@@ -721,6 +861,11 @@ class RemoteFilesystem
             return $caPath = $envCertFile;
         }
 
+        $configured = ini_get('openssl.cafile');
+        if ($configured && strlen($configured) > 0 && is_readable($configured) && $this->validateCaFile($configured)) {
+            return $caPath = $configured;
+        }
+
         $caBundlePaths = array(
             '/etc/pki/tls/certs/ca-bundle.crt', // Fedora, RHEL, CentOS (ca-certificates package)
             '/etc/ssl/certs/ca-certificates.crt', // Debian, Ubuntu, Gentoo, Arch Linux (ca-certificates package)
@@ -732,16 +877,10 @@ class RemoteFilesystem
             '/usr/share/ssl/certs/ca-bundle.crt', // Really old RedHat?
             '/etc/ssl/cert.pem', // OpenBSD
             '/usr/local/etc/ssl/cert.pem', // FreeBSD 10.x
-            __DIR__.'/../../../res/cacert.pem', // Bundled with Composer
         );
 
-        $configured = ini_get('openssl.cafile');
-        if ($configured && strlen($configured) > 0 && is_readable($configured) && $this->validateCaFile($configured)) {
-            return $caPath = $configured;
-        }
-
         foreach ($caBundlePaths as $caBundle) {
-            if (@is_readable($caBundle) && $this->validateCaFile($caBundle)) {
+            if (Silencer::call('is_readable', $caBundle) && $this->validateCaFile($caBundle)) {
                 return $caPath = $caBundle;
             }
         }
@@ -753,26 +892,124 @@ class RemoteFilesystem
             }
         }
 
-        return $caPath = false;
+        return $caPath = __DIR__.'/../../../res/cacert.pem'; // Bundled with Composer, last resort
     }
 
+    /**
+     * @param string $filename
+     *
+     * @return bool
+     */
     private function validateCaFile($filename)
     {
-        if ($this->io->isDebug()) {
-            $this->io->writeError('Checking CA file '.realpath($filename));
+        static $files = array();
+
+        if (isset($files[$filename])) {
+            return $files[$filename];
         }
+
+        $this->io->writeError('Checking CA file '.realpath($filename), true, IOInterface::DEBUG);
         $contents = file_get_contents($filename);
 
         // assume the CA is valid if php is vulnerable to
         // https://www.sektioneins.de/advisories/advisory-012013-php-openssl_x509_parse-memory-corruption-vulnerability.html
-        if (
-            PHP_VERSION_ID <= 50327
-            || (PHP_VERSION_ID >= 50400 && PHP_VERSION_ID < 50422)
-            || (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50506)
-        ) {
-            return !empty($contents);
+        if (!TlsHelper::isOpensslParseSafe()) {
+            $this->io->writeError(sprintf(
+                '<error>Your version of PHP, %s, is affected by CVE-2013-6420 and cannot safely perform certificate validation, we strongly suggest you upgrade.</error>',
+                PHP_VERSION
+            ));
+
+            return $files[$filename] = !empty($contents);
         }
 
-        return (bool) openssl_x509_parse($contents);
+        return $files[$filename] = (bool) openssl_x509_parse($contents);
+    }
+
+    /**
+     * Uses stream_copy_to_stream instead of copy to work around https://bugs.php.net/bug.php?id=64634
+     *
+     * @param string $source
+     * @param string $target
+     */
+    private function streamCopy($source, $target)
+    {
+        $source = fopen($source, 'r');
+        $target = fopen($target, 'w+');
+
+        stream_copy_to_stream($source, $target);
+        fclose($source);
+        fclose($target);
+
+        unset($source, $target);
+    }
+
+    /**
+     * Fetch certificate common name and fingerprint for validation of SAN.
+     *
+     * @todo Remove when PHP 5.6 is minimum supported version.
+     */
+    private function getCertificateCnAndFp($url, $options)
+    {
+        if (PHP_VERSION_ID >= 50600) {
+            throw new \BadMethodCallException(sprintf(
+                '%s must not be used on PHP >= 5.6',
+                __METHOD__
+            ));
+        }
+
+        $context = StreamContextFactory::getContext($url, $options, array('options' => array(
+            'ssl' => array(
+                'capture_peer_cert' => true,
+                'verify_peer' => false, // Yes this is fucking insane! But PHP is lame.
+            ), ),
+        ));
+
+        // Ideally this would just use stream_socket_client() to avoid sending a
+        // HTTP request but that does not capture the certificate.
+        if (false === $handle = @fopen($url, 'rb', false, $context)) {
+            return;
+        }
+
+        // Close non authenticated connection without reading any content.
+        fclose($handle);
+        $handle = null;
+
+        $params = stream_context_get_params($context);
+
+        if (!empty($params['options']['ssl']['peer_certificate'])) {
+            $peerCertificate = $params['options']['ssl']['peer_certificate'];
+
+            if (TlsHelper::checkCertificateHost($peerCertificate, parse_url($url, PHP_URL_HOST), $commonName)) {
+                return array(
+                    'cn' => $commonName,
+                    'fp' => TlsHelper::getCertificateFingerprint($peerCertificate),
+                );
+            }
+        }
+    }
+
+    private function getUrlAuthority($url)
+    {
+        $defaultPorts = array(
+            'ftp' => 21,
+            'http' => 80,
+            'https' => 443,
+            'ssh2.sftp' => 22,
+            'ssh2.scp' => 22,
+        );
+
+        $scheme = parse_url($url, PHP_URL_SCHEME);
+
+        if (!isset($defaultPorts[$scheme])) {
+            throw new \InvalidArgumentException(sprintf(
+                'Could not get default port for unknown scheme: %s',
+                $scheme
+            ));
+        }
+
+        $defaultPort = $defaultPorts[$scheme];
+        $port = parse_url($url, PHP_URL_PORT) ?: $defaultPort;
+
+        return parse_url($url, PHP_URL_HOST).':'.$port;
     }
 }

+ 77 - 0
src/Composer/Util/Silencer.php

@@ -0,0 +1,77 @@
+<?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;
+
+/**
+ * Temporarily suppress PHP error reporting, usually warnings and below.
+ *
+ * @author Niels Keurentjes <niels.keurentjes@omines.com>
+ */
+class Silencer
+{
+    /**
+     * @var int[] Unpop stack
+     */
+    private static $stack = array();
+
+    /**
+     * Suppresses given mask or errors.
+     *
+     * @param  int|null $mask Error levels to suppress, default value NULL indicates all warnings and below.
+     * @return int      The old error reporting level.
+     */
+    public static function suppress($mask = null)
+    {
+        if (!isset($mask)) {
+            $mask = E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_DEPRECATED | E_USER_DEPRECATED | E_STRICT;
+        }
+        $old = error_reporting();
+        array_push(self::$stack, $old);
+        error_reporting($old & ~$mask);
+
+        return $old;
+    }
+
+    /**
+     * Restores a single state.
+     */
+    public static function restore()
+    {
+        if (!empty(self::$stack)) {
+            error_reporting(array_pop(self::$stack));
+        }
+    }
+
+    /**
+     * Calls a specified function while silencing warnings and below.
+     *
+     * Future improvement: when PHP requirements are raised add Callable type hint (5.4) and variadic parameters (5.6)
+     *
+     * @param  callable   $callable Function to execute.
+     * @throws \Exception Any exceptions from the callback are rethrown.
+     * @return mixed      Return value of the callback.
+     */
+    public static function call($callable /*, ...$parameters */)
+    {
+        try {
+            self::suppress();
+            $result = call_user_func_array($callable, array_slice(func_get_args(), 1));
+            self::restore();
+
+            return $result;
+        } catch (\Exception $e) {
+            // Use a finally block for this when requirements are raised to PHP 5.5
+            self::restore();
+            throw $e;
+        }
+    }
+}

File diff suppressed because it is too large
+ 210 - 0
src/Composer/Util/TlsHelper.php


+ 12 - 7
tests/Composer/Test/AllFunctionalTest.php

@@ -12,14 +12,15 @@
 
 namespace Composer\Test;
 
-use Symfony\Component\Process\Process;
+use Composer\TestCase;
 use Composer\Util\Filesystem;
 use Symfony\Component\Finder\Finder;
+use Symfony\Component\Process\Process;
 
 /**
  * @group slow
  */
-class AllFunctionalTest extends \PHPUnit_Framework_TestCase
+class AllFunctionalTest extends TestCase
 {
     protected $oldcwd;
     protected $oldenv;
@@ -29,17 +30,21 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase
     public function setUp()
     {
         $this->oldcwd = getcwd();
+
         chdir(__DIR__.'/Fixtures/functional');
     }
 
     public function tearDown()
     {
         chdir($this->oldcwd);
+
         $fs = new Filesystem;
+
         if ($this->testDir) {
             $fs->removeDirectory($this->testDir);
             $this->testDir = null;
         }
+
         if ($this->oldenv) {
             $fs->removeDirectory(getenv('COMPOSER_HOME'));
             $_SERVER['COMPOSER_HOME'] = $this->oldenv;
@@ -50,7 +55,7 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase
 
     public static function setUpBeforeClass()
     {
-        self::$pharPath = sys_get_temp_dir().'/composer-phar-test/composer.phar';
+        self::$pharPath = self::getUniqueTmpDirectory() . '/composer.phar';
     }
 
     public static function tearDownAfterClass()
@@ -66,9 +71,7 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase
         }
 
         $target = dirname(self::$pharPath);
-        $fs = new Filesystem;
-        $fs->removeDirectory($target);
-        $fs->ensureDirectoryExists($target);
+        $fs = new Filesystem();
         chdir($target);
 
         $it = new \RecursiveDirectoryIterator(__DIR__.'/../../../', \RecursiveDirectoryIterator::SKIP_DOTS);
@@ -85,9 +88,11 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase
 
         $proc = new Process('php '.escapeshellarg('./bin/compile'), $target);
         $exitcode = $proc->run();
+
         if ($exitcode !== 0 || trim($proc->getOutput())) {
             $this->fail($proc->getOutput());
         }
+
         $this->assertTrue(file_exists(self::$pharPath));
     }
 
@@ -140,7 +145,7 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase
         $data = array();
         $section = null;
 
-        $testDir = sys_get_temp_dir().'/composer_functional_test'.uniqid(mt_rand(), true);
+        $testDir = self::getUniqueTmpDirectory();
         $this->testDir = $testDir;
         $varRegex = '#%([a-zA-Z_-]+)%#';
         $variableReplacer = function ($match) use (&$data, $testDir) {

+ 9 - 0
tests/Composer/Test/ApplicationTest.php

@@ -14,6 +14,7 @@ namespace Composer\Test;
 
 use Composer\Console\Application;
 use Composer\TestCase;
+use Symfony\Component\Console\Output\OutputInterface;
 
 class ApplicationTest extends TestCase
 {
@@ -30,11 +31,19 @@ class ApplicationTest extends TestCase
 
         $index = 0;
         if (extension_loaded('xdebug')) {
+            $outputMock->expects($this->at($index++))
+                ->method("getVerbosity")
+                ->willReturn(OutputInterface::VERBOSITY_NORMAL);
+
             $outputMock->expects($this->at($index++))
                 ->method("write")
                 ->with($this->equalTo('<warning>You are running composer with xdebug enabled. This has a major impact on runtime performance. See https://getcomposer.org/xdebug</warning>'));
         }
 
+        $outputMock->expects($this->at($index++))
+            ->method("getVerbosity")
+            ->willReturn(OutputInterface::VERBOSITY_NORMAL);
+
         $outputMock->expects($this->at($index++))
             ->method("write")
             ->with($this->equalTo(sprintf('<warning>Warning: This development build of composer is over 60 days old. It is recommended to update it by running "%s self-update" to get the latest version.</warning>', $_SERVER['PHP_SELF'])));

+ 2 - 2
tests/Composer/Test/Autoload/AutoloadGeneratorTest.php

@@ -88,8 +88,7 @@ class AutoloadGeneratorTest extends TestCase
         $this->fs = new Filesystem;
         $that = $this;
 
-        $this->workingDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest-'.md5(uniqid('', true));
-        $this->fs->ensureDirectoryExists($this->workingDir);
+        $this->workingDir = $this->getUniqueTmpDirectory();
         $this->vendorDir = $this->workingDir.DIRECTORY_SEPARATOR.'composer-test-autoload';
         $this->ensureDirectoryExistsAndClear($this->vendorDir);
 
@@ -144,6 +143,7 @@ class AutoloadGeneratorTest extends TestCase
         if (is_dir($this->workingDir)) {
             $this->fs->removeDirectory($this->workingDir);
         }
+
         if (is_dir($this->vendorDir)) {
             $this->fs->removeDirectory($this->vendorDir);
         }

+ 5 - 10
tests/Composer/Test/Autoload/ClassMapGeneratorTest.php

@@ -19,10 +19,11 @@
 namespace Composer\Test\Autoload;
 
 use Composer\Autoload\ClassMapGenerator;
+use Composer\TestCase;
 use Symfony\Component\Finder\Finder;
 use Composer\Util\Filesystem;
 
-class ClassMapGeneratorTest extends \PHPUnit_Framework_TestCase
+class ClassMapGeneratorTest extends TestCase
 {
     /**
      * @dataProvider getTestCreateMapTests
@@ -127,10 +128,8 @@ class ClassMapGeneratorTest extends \PHPUnit_Framework_TestCase
     {
         $this->checkIfFinderIsAvailable();
 
-        $tempDir = sys_get_temp_dir().'/ComposerTestAmbiguousRefs';
-        if (!is_dir($tempDir.'/other')) {
-            mkdir($tempDir.'/other', 0777, true);
-        }
+        $tempDir = $this->getUniqueTmpDirectory();
+        $this->ensureDirectoryExistsAndClear($tempDir.'/other');
 
         $finder = new Finder();
         $finder->files()->in($tempDir);
@@ -171,13 +170,9 @@ class ClassMapGeneratorTest extends \PHPUnit_Framework_TestCase
      */
     public function testUnambiguousReference()
     {
-        $tempDir = sys_get_temp_dir().'/ComposerTestUnambiguousRefs';
-        if (!is_dir($tempDir)) {
-            mkdir($tempDir, 0777, true);
-        }
+        $tempDir = $this->getUniqueTmpDirectory();
 
         file_put_contents($tempDir.'/A.php', "<?php\nclass A {}");
-
         file_put_contents(
             $tempDir.'/B.php',
             "<?php

+ 3 - 3
tests/Composer/Test/CacheTest.php

@@ -25,15 +25,15 @@ class CacheTest extends TestCase
             $this->markTestSkipped('Test causes intermittent failures on Travis');
         }
 
-        $this->root = sys_get_temp_dir() . '/composer_testdir';
-        $this->ensureDirectoryExistsAndClear($this->root);
-
+        $this->root = $this->getUniqueTmpDirectory();
         $this->files = array();
         $zeros = str_repeat('0', 1000);
+
         for ($i = 0; $i < 4; $i++) {
             file_put_contents("{$this->root}/cached.file{$i}.zip", $zeros);
             $this->files[] = new \SplFileInfo("{$this->root}/cached.file{$i}.zip");
         }
+
         $this->finder = $this->getMockBuilder('Symfony\Component\Finder\Finder')->disableOriginalConstructor()->getMock();
 
         $io = $this->getMock('Composer\IO\IOInterface');

+ 15 - 0
tests/Composer/Test/Config/Fixtures/config/config-with-exampletld-repository-and-options.json

@@ -0,0 +1,15 @@
+{
+    "name": "my-vend/my-app",
+    "license": "MIT",
+    "repositories": {
+        "example_tld": {
+            "type": "composer",
+            "url": "https://example.tld",
+            "options": {
+                "ssl": {
+                    "local_cert": "/home/composer/.ssl/composer.pem"
+                }
+            }
+        }
+    }
+}

+ 21 - 3
tests/Composer/Test/Config/JsonConfigSourceTest.php

@@ -14,9 +14,10 @@ namespace Composer\Test\Json;
 
 use Composer\Config\JsonConfigSource;
 use Composer\Json\JsonFile;
+use Composer\TestCase;
 use Composer\Util\Filesystem;
 
-class JsonConfigSourceTest extends \PHPUnit_Framework_TestCase
+class JsonConfigSourceTest extends TestCase
 {
     /** @var Filesystem */
     private $fs;
@@ -31,8 +32,7 @@ class JsonConfigSourceTest extends \PHPUnit_Framework_TestCase
     protected function setUp()
     {
         $this->fs = new Filesystem;
-        $this->workingDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest';
-        $this->fs->ensureDirectoryExists($this->workingDir);
+        $this->workingDir = $this->getUniqueTmpDirectory();
     }
 
     protected function tearDown()
@@ -52,6 +52,24 @@ class JsonConfigSourceTest extends \PHPUnit_Framework_TestCase
         $this->assertFileEquals($this->fixturePath('config/config-with-exampletld-repository.json'), $config);
     }
 
+    public function testAddRepositoryWithOptions()
+    {
+        $config = $this->workingDir.'/composer.json';
+        copy($this->fixturePath('composer-repositories.json'), $config);
+        $jsonConfigSource = new JsonConfigSource(new JsonFile($config));
+        $jsonConfigSource->addRepository('example_tld', array(
+            'type' => 'composer',
+            'url' => 'https://example.tld',
+            'options' => array(
+                'ssl' => array(
+                    'local_cert' => '/home/composer/.ssl/composer.pem',
+                ),
+            ),
+        ));
+
+        $this->assertFileEquals($this->fixturePath('config/config-with-exampletld-repository-and-options.json'), $config);
+    }
+
     public function testRemoveRepository()
     {
         $config = $this->workingDir.'/composer.json';

+ 10 - 0
tests/Composer/Test/ConfigTest.php

@@ -148,6 +148,16 @@ class ConfigTest extends \PHPUnit_Framework_TestCase
         $this->assertEquals('/baz', $config->get('cache-dir'));
     }
 
+    public function testStreamWrapperDirs()
+    {
+        $config = new Config(false, '/foo/bar');
+        $config->merge(array('config' => array(
+            'cache-dir' => 's3://baz/',
+        )));
+
+        $this->assertEquals('s3://baz', $config->get('cache-dir'));
+    }
+
     public function testFetchingRelativePaths()
     {
         $config = new Config(false, '/foo/bar');

+ 1 - 2
tests/Composer/Test/DefaultConfigTest.php

@@ -24,5 +24,4 @@ class DefaultConfigTest extends \PHPUnit_Framework_TestCase
         $config = new Config;
         $this->assertFalse($config->get('disable-tls'));
     }
-
-}
+}

+ 1 - 1
tests/Composer/Test/DependencyResolver/SolverTest.php

@@ -709,7 +709,7 @@ class SolverTest extends TestCase
             $msg .= "Potential causes:\n";
             $msg .= " - A typo in the package name\n";
             $msg .= " - The package is not available in a stable-enough version according to your minimum-stability setting\n";
-            $msg .= "   see <https://groups.google.com/d/topic/composer-dev/_g3ASeIFlrc/discussion> for more details.\n\n";
+            $msg .= "   see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for more details.\n\n";
             $msg .= "Read <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.";
             $this->assertEquals($msg, $e->getMessage());
         }

+ 6 - 13
tests/Composer/Test/Downloader/FileDownloaderTest.php

@@ -13,9 +13,10 @@
 namespace Composer\Test\Downloader;
 
 use Composer\Downloader\FileDownloader;
+use Composer\TestCase;
 use Composer\Util\Filesystem;
 
-class FileDownloaderTest extends \PHPUnit_Framework_TestCase
+class FileDownloaderTest extends TestCase
 {
     protected function getDownloader($io = null, $config = null, $eventDispatcher = null, $cache = null, $rfs = null, $filesystem = null)
     {
@@ -53,9 +54,9 @@ class FileDownloaderTest extends \PHPUnit_Framework_TestCase
             ->will($this->returnValue(array('url')))
         ;
 
-        $path = tempnam(sys_get_temp_dir(), 'c');
-
+        $path = tempnam($this->getUniqueTmpDirectory(), 'c');
         $downloader = $this->getDownloader();
+
         try {
             $downloader->download($packageMock, $path);
             $this->fail();
@@ -102,10 +103,7 @@ class FileDownloaderTest extends \PHPUnit_Framework_TestCase
             ->will($this->returnValue(array()))
         ;
 
-        do {
-            $path = sys_get_temp_dir().'/'.md5(time().mt_rand());
-        } while (file_exists($path));
-
+        $path = $this->getUniqueTmpDirectory();
         $ioMock = $this->getMock('Composer\IO\IOInterface');
         $ioMock->expects($this->any())
             ->method('write')
@@ -187,14 +185,9 @@ class FileDownloaderTest extends \PHPUnit_Framework_TestCase
         ;
         $filesystem = $this->getMock('Composer\Util\Filesystem');
 
-        do {
-            $path = sys_get_temp_dir().'/'.md5(time().mt_rand());
-        } while (file_exists($path));
-
+        $path = $this->getUniqueTmpDirectory();
         $downloader = $this->getDownloader(null, null, null, null, null, $filesystem);
-
         // make sure the file expected to be downloaded is on disk already
-        mkdir($path, 0777, true);
         touch($path.'/script.js');
 
         try {

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

@@ -14,9 +14,11 @@ namespace Composer\Test\Downloader;
 
 use Composer\Downloader\GitDownloader;
 use Composer\Config;
+use Composer\TestCase;
 use Composer\Util\Filesystem;
+use Composer\Util\Platform;
 
-class GitDownloaderTest extends \PHPUnit_Framework_TestCase
+class GitDownloaderTest extends TestCase
 {
     /** @var Filesystem */
     private $fs;
@@ -26,7 +28,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase
     protected function setUp()
     {
         $this->fs = new Filesystem;
-        $this->workingDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest-'.md5(uniqid('', true));
+        $this->workingDir = $this->getUniqueTmpDirectory();
     }
 
     protected function tearDown()
@@ -317,7 +319,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase
             ->method('execute')
             ->with($this->equalTo($expectedGitUpdateCommand))
             ->will($this->returnValue(1));
-        
+
         $this->fs->ensureDirectoryExists($this->workingDir.'/.git');
         $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor);
         $downloader->update($packageMock, $packageMock, $this->workingDir);
@@ -352,7 +354,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase
 
     private function winCompat($cmd)
     {
-        if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+        if (Platform::isWindows()) {
             $cmd = str_replace('cd ', 'cd /D ', $cmd);
             $cmd = str_replace('composerPath', getcwd().'/composerPath', $cmd);
 

+ 5 - 7
tests/Composer/Test/Downloader/HgDownloaderTest.php

@@ -13,16 +13,18 @@
 namespace Composer\Test\Downloader;
 
 use Composer\Downloader\HgDownloader;
+use Composer\TestCase;
 use Composer\Util\Filesystem;
+use Composer\Util\Platform;
 
-class HgDownloaderTest extends \PHPUnit_Framework_TestCase
+class HgDownloaderTest extends TestCase
 {
     /** @var string */
     private $workingDir;
 
     protected function setUp()
     {
-        $this->workingDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest-'.md5(uniqid('', true));
+        $this->workingDir = $this->getUniqueTmpDirectory();
     }
 
     protected function tearDown()
@@ -155,10 +157,6 @@ class HgDownloaderTest extends \PHPUnit_Framework_TestCase
 
     private function getCmd($cmd)
     {
-        if (defined('PHP_WINDOWS_VERSION_BUILD')) {
-            return strtr($cmd, "'", '"');
-        }
-
-        return $cmd;
+        return Platform::isWindows() ? strtr($cmd, "'", '"') : $cmd;
     }
 }

+ 3 - 2
tests/Composer/Test/Downloader/PearPackageExtractorTest.php

@@ -13,8 +13,9 @@
 namespace Composer\Test\Downloader;
 
 use Composer\Downloader\PearPackageExtractor;
+use Composer\TestCase;
 
-class PearPackageExtractorTest extends \PHPUnit_Framework_TestCase
+class PearPackageExtractorTest extends TestCase
 {
     public function testShouldExtractPackage_1_0()
     {
@@ -122,7 +123,7 @@ class PearPackageExtractorTest extends \PHPUnit_Framework_TestCase
 
     public function testShouldPerformReplacements()
     {
-        $from = tempnam(sys_get_temp_dir(), 'pear-extract');
+        $from = tempnam($this->getUniqueTmpDirectory(), 'pear-extract');
         $to = $from.'-to';
 
         $original = 'replaced: @placeholder@; not replaced: @another@; replaced again: @placeholder@';

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

@@ -16,12 +16,13 @@ use Composer\Downloader\PerforceDownloader;
 use Composer\Config;
 use Composer\Repository\VcsRepository;
 use Composer\IO\IOInterface;
+use Composer\TestCase;
 use Composer\Util\Filesystem;
 
 /**
  * @author Matt Whittom <Matt.Whittom@veteransunited.com>
  */
-class PerforceDownloaderTest extends \PHPUnit_Framework_TestCase
+class PerforceDownloaderTest extends TestCase
 {
     protected $config;
     protected $downloader;
@@ -34,7 +35,7 @@ class PerforceDownloaderTest extends \PHPUnit_Framework_TestCase
 
     protected function setUp()
     {
-        $this->testPath        = sys_get_temp_dir() . '/composer-test';
+        $this->testPath        = $this->getUniqueTmpDirectory();
         $this->repoConfig      = $this->getRepoConfig();
         $this->config          = $this->getConfig();
         $this->io              = $this->getMockIoInterface();

+ 6 - 4
tests/Composer/Test/Downloader/XzDownloaderTest.php

@@ -13,10 +13,12 @@
 namespace Composer\Test\Downloader;
 
 use Composer\Downloader\XzDownloader;
+use Composer\TestCase;
 use Composer\Util\Filesystem;
+use Composer\Util\Platform;
 use Composer\Util\RemoteFilesystem;
 
-class XzDownloaderTest extends \PHPUnit_Framework_TestCase
+class XzDownloaderTest extends TestCase
 {
     /**
      * @var Filesystem
@@ -30,10 +32,10 @@ class XzDownloaderTest extends \PHPUnit_Framework_TestCase
 
     public function setUp()
     {
-        if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+        if (Platform::isWindows()) {
             $this->markTestSkipped('Skip test on Windows');
         }
-        $this->testDir = sys_get_temp_dir().'/composer-xz-test-vendor';
+        $this->testDir = $this->getUniqueTmpDirectory();
     }
 
     public function tearDown()
@@ -67,7 +69,7 @@ class XzDownloaderTest extends \PHPUnit_Framework_TestCase
         $downloader = new XzDownloader($io, $config, null, null, null, new RemoteFilesystem($io));
 
         try {
-            $downloader->download($packageMock, sys_get_temp_dir().'/composer-xz-test');
+            $downloader->download($packageMock, $this->getUniqueTmpDirectory());
             $this->fail('Download of invalid tarball should throw an exception');
         } catch (\RuntimeException $e) {
             $this->assertContains('File format not recognized', $e->getMessage());

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

@@ -13,11 +13,11 @@
 namespace Composer\Test\Downloader;
 
 use Composer\Downloader\ZipDownloader;
+use Composer\TestCase;
 use Composer\Util\Filesystem;
 
-class ZipDownloaderTest extends \PHPUnit_Framework_TestCase
+class ZipDownloaderTest extends TestCase
 {
-
     /**
      * @var string
      */
@@ -28,7 +28,8 @@ class ZipDownloaderTest extends \PHPUnit_Framework_TestCase
         if (!class_exists('ZipArchive')) {
             $this->markTestSkipped('zip extension missing');
         }
-        $this->testDir = sys_get_temp_dir().'/composer-zip-test-vendor';
+
+        $this->testDir = $this->getUniqueTmpDirectory();
     }
 
     public function tearDown()
@@ -64,6 +65,10 @@ class ZipDownloaderTest extends \PHPUnit_Framework_TestCase
             ->with('cafile')
             ->will($this->returnValue(null));
         $config->expects($this->at(2))
+            ->method('get')
+            ->with('capath')
+            ->will($this->returnValue(null));
+        $config->expects($this->at(3))
             ->method('get')
             ->with('vendor-dir')
             ->will($this->returnValue($this->testDir));

+ 15 - 42
tests/Composer/Test/EventDispatcher/EventDispatcherTest.php

@@ -15,9 +15,11 @@ namespace Composer\Test\EventDispatcher;
 use Composer\EventDispatcher\Event;
 use Composer\Installer\InstallerEvents;
 use Composer\TestCase;
+use Composer\IO\BufferIO;
 use Composer\Script\ScriptEvents;
 use Composer\Script\CommandEvent;
 use Composer\Util\ProcessExecutor;
+use Symfony\Component\Console\Output\OutputInterface;
 
 class EventDispatcherTest extends TestCase
 {
@@ -101,7 +103,7 @@ class EventDispatcherTest extends TestCase
         $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
             ->setConstructorArgs(array(
                 $this->getMock('Composer\Composer'),
-                $io = $this->getMock('Composer\IO\IOInterface'),
+                $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE),
                 $process,
             ))
             ->setMethods(array(
@@ -123,23 +125,12 @@ class EventDispatcherTest extends TestCase
             ->method('getListeners')
             ->will($this->returnValue($listeners));
 
-        $io->expects($this->any())
-            ->method('isVerbose')
-            ->willReturn(1);
-
-        $io->expects($this->at(1))
-            ->method('writeError')
-            ->with($this->equalTo('> post-install-cmd: echo -n foo'));
-
-        $io->expects($this->at(3))
-            ->method('writeError')
-            ->with($this->equalTo('> post-install-cmd: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'));
-
-        $io->expects($this->at(5))
-            ->method('writeError')
-            ->with($this->equalTo('> post-install-cmd: echo -n bar'));
-
         $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false);
+
+        $expected = '> post-install-cmd: echo -n foo'.PHP_EOL.
+            '> post-install-cmd: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'.PHP_EOL.
+            '> post-install-cmd: echo -n bar'.PHP_EOL;
+        $this->assertEquals($expected, $io->getOutput());
     }
 
     public function testDispatcherCanExecuteComposerScriptGroups()
@@ -148,7 +139,7 @@ class EventDispatcherTest extends TestCase
         $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
             ->setConstructorArgs(array(
                 $composer = $this->getMock('Composer\Composer'),
-                $io = $this->getMock('Composer\IO\IOInterface'),
+                $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE),
                 $process,
             ))
             ->setMethods(array(
@@ -174,31 +165,13 @@ class EventDispatcherTest extends TestCase
                 return array();
             }));
 
-        $io->expects($this->any())
-            ->method('isVerbose')
-            ->willReturn(1);
-
-        $io->expects($this->at(1))
-            ->method('writeError')
-            ->with($this->equalTo('> root: @group'));
-
-        $io->expects($this->at(3))
-            ->method('writeError')
-            ->with($this->equalTo('> group: echo -n foo'));
-
-        $io->expects($this->at(5))
-            ->method('writeError')
-            ->with($this->equalTo('> group: @subgroup'));
-
-        $io->expects($this->at(7))
-            ->method('writeError')
-            ->with($this->equalTo('> subgroup: echo -n baz'));
-
-        $io->expects($this->at(9))
-            ->method('writeError')
-            ->with($this->equalTo('> group: echo -n bar'));
-
         $dispatcher->dispatch('root', new CommandEvent('root', $composer, $io));
+        $expected = '> root: @group'.PHP_EOL.
+            '> group: echo -n foo'.PHP_EOL.
+            '> group: @subgroup'.PHP_EOL.
+            '> subgroup: echo -n baz'.PHP_EOL.
+            '> group: echo -n bar'.PHP_EOL;
+        $this->assertEquals($expected, $io->getOutput());
     }
 
     /**

+ 4 - 4
tests/Composer/Test/Fixtures/installer/abandoned-listed.test

@@ -24,12 +24,12 @@ Abandoned packages are flagged
 --RUN--
 install
 --EXPECT-OUTPUT--
-<info>Loading composer repositories with package information</info>
-<info>Installing dependencies (including require-dev)</info>
+Loading composer repositories with package information
+Installing dependencies (including require-dev)
 <warning>Package a/a is abandoned, you should avoid using it. No replacement was suggested.</warning>
 <warning>Package c/c is abandoned, you should avoid using it. Use b/b instead.</warning>
-<info>Writing lock file</info>
-<info>Generating autoload files</info>
+Writing lock file
+Generating autoload files
 
 --EXPECT--
 Installing a/a (1.0.0)

+ 4 - 4
tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test

@@ -21,9 +21,9 @@ Broken dependencies should not lead to a replacer being installed which is not m
 --RUN--
 install
 --EXPECT-OUTPUT--
-<info>Loading composer repositories with package information</info>
-<info>Installing dependencies (including require-dev)</info>
-<error>Your requirements could not be resolved to an installable set of packages.</error>
+Loading composer repositories with package information
+Installing dependencies (including require-dev)
+Your requirements could not be resolved to an installable set of packages.
 
   Problem 1
     - c/c 1.0.0 requires x/x 1.0 -> no matching package found.
@@ -33,7 +33,7 @@ install
 Potential causes:
  - A typo in the package name
  - The package is not available in a stable-enough version according to your minimum-stability setting
-   see <https://groups.google.com/d/topic/composer-dev/_g3ASeIFlrc/discussion> for more details.
+   see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for more details.
 
 Read <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.
 

+ 16 - 0
tests/Composer/Test/Fixtures/installer/install-self-from-root.test

@@ -0,0 +1,16 @@
+--TEST--
+Tries to require a package with the same name as the root package
+--COMPOSER--
+{
+    "name": "foo/bar",
+    "require": {
+        "foo/bar": "@dev"
+    }
+}
+--RUN--
+install
+--EXPECT-EXCEPTION--
+InvalidArgumentException
+--EXPECT--
+Root package 'foo/bar' cannot require itself in its composer.json
+Did you accidentally name your root package after an external package?

+ 4 - 4
tests/Composer/Test/Fixtures/installer/suggest-installed.test

@@ -19,10 +19,10 @@ Suggestions are not displayed for installed packages
 --RUN--
 install
 --EXPECT-OUTPUT--
-<info>Loading composer repositories with package information</info>
-<info>Installing dependencies (including require-dev)</info>
-<info>Writing lock file</info>
-<info>Generating autoload files</info>
+Loading composer repositories with package information
+Installing dependencies (including require-dev)
+Writing lock file
+Generating autoload files
 
 --EXPECT--
 Installing a/a (1.0.0)

+ 4 - 4
tests/Composer/Test/Fixtures/installer/suggest-prod.test

@@ -17,10 +17,10 @@ Suggestions are not displayed in non-dev mode
 --RUN--
 install --no-dev
 --EXPECT-OUTPUT--
-<info>Loading composer repositories with package information</info>
-<info>Installing dependencies</info>
-<info>Writing lock file</info>
-<info>Generating autoload files</info>
+Loading composer repositories with package information
+Installing dependencies
+Writing lock file
+Generating autoload files
 
 --EXPECT--
 Installing a/a (1.0.0)

+ 4 - 4
tests/Composer/Test/Fixtures/installer/suggest-replaced.test

@@ -19,10 +19,10 @@ Suggestions are not displayed for packages if they are replaced
 --RUN--
 install
 --EXPECT-OUTPUT--
-<info>Loading composer repositories with package information</info>
-<info>Installing dependencies (including require-dev)</info>
-<info>Writing lock file</info>
-<info>Generating autoload files</info>
+Loading composer repositories with package information
+Installing dependencies (including require-dev)
+Writing lock file
+Generating autoload files
 
 --EXPECT--
 Installing c/c (1.0.0)

+ 4 - 4
tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test

@@ -17,11 +17,11 @@ Suggestions are displayed
 --RUN--
 install
 --EXPECT-OUTPUT--
-<info>Loading composer repositories with package information</info>
-<info>Installing dependencies (including require-dev)</info>
+Loading composer repositories with package information
+Installing dependencies (including require-dev)
 a/a suggests installing b/b (an obscure reason)
-<info>Writing lock file</info>
-<info>Generating autoload files</info>
+Writing lock file
+Generating autoload files
 
 --EXPECT--
 Installing a/a (1.0.0)

+ 20 - 7
tests/Composer/Test/IO/ConsoleIOTest.php

@@ -14,6 +14,7 @@ namespace Composer\Test\IO;
 
 use Composer\IO\ConsoleIO;
 use Composer\TestCase;
+use Symfony\Component\Console\Output\OutputInterface;
 
 class ConsoleIOTest extends TestCase
 {
@@ -40,6 +41,9 @@ class ConsoleIOTest extends TestCase
     {
         $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
         $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+        $outputMock->expects($this->once())
+            ->method('getVerbosity')
+            ->willReturn(OutputInterface::VERBOSITY_NORMAL);
         $outputMock->expects($this->once())
             ->method('write')
             ->with($this->equalTo('some information about something'), $this->equalTo(false));
@@ -53,6 +57,9 @@ class ConsoleIOTest extends TestCase
     {
         $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
         $outputMock = $this->getMock('Symfony\Component\Console\Output\ConsoleOutputInterface');
+        $outputMock->expects($this->once())
+            ->method('getVerbosity')
+            ->willReturn(OutputInterface::VERBOSITY_NORMAL);
         $outputMock->expects($this->once())
             ->method('getErrorOutput')
             ->willReturn($outputMock);
@@ -69,6 +76,9 @@ class ConsoleIOTest extends TestCase
     {
         $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
         $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+        $outputMock->expects($this->once())
+            ->method('getVerbosity')
+            ->willReturn(OutputInterface::VERBOSITY_NORMAL);
         $outputMock->expects($this->once())
             ->method('write')
             ->with(
@@ -95,25 +105,28 @@ class ConsoleIOTest extends TestCase
         $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
         $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
 
-        $outputMock->expects($this->at(0))
+        $outputMock->expects($this->any())
+            ->method('getVerbosity')
+            ->willReturn(OutputInterface::VERBOSITY_NORMAL);
+        $outputMock->expects($this->at(1))
             ->method('write')
             ->with($this->equalTo('something (<question>strlen = 23</question>)'));
-        $outputMock->expects($this->at(1))
+        $outputMock->expects($this->at(3))
             ->method('write')
             ->with($this->equalTo(str_repeat("\x08", 23)), $this->equalTo(false));
-        $outputMock->expects($this->at(2))
+        $outputMock->expects($this->at(5))
             ->method('write')
             ->with($this->equalTo('shorter (<comment>12</comment>)'), $this->equalTo(false));
-        $outputMock->expects($this->at(3))
+        $outputMock->expects($this->at(7))
             ->method('write')
             ->with($this->equalTo(str_repeat(' ', 11)), $this->equalTo(false));
-        $outputMock->expects($this->at(4))
+        $outputMock->expects($this->at(9))
             ->method('write')
             ->with($this->equalTo(str_repeat("\x08", 11)), $this->equalTo(false));
-        $outputMock->expects($this->at(5))
+        $outputMock->expects($this->at(11))
             ->method('write')
             ->with($this->equalTo(str_repeat("\x08", 12)), $this->equalTo(false));
-        $outputMock->expects($this->at(6))
+        $outputMock->expects($this->at(13))
             ->method('write')
             ->with($this->equalTo('something longer than initial (<info>34</info>)'));
 

+ 5 - 4
tests/Composer/Test/Installer/LibraryInstallerTest.php

@@ -22,6 +22,7 @@ class LibraryInstallerTest extends TestCase
 {
     protected $composer;
     protected $config;
+    protected $rootDir;
     protected $vendorDir;
     protected $binDir;
     protected $dm;
@@ -37,10 +38,11 @@ class LibraryInstallerTest extends TestCase
         $this->config = new Config();
         $this->composer->setConfig($this->config);
 
-        $this->vendorDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'composer-test-vendor';
+        $this->rootDir = $this->getUniqueTmpDirectory();
+        $this->vendorDir = $this->rootDir.DIRECTORY_SEPARATOR.'vendor';
         $this->ensureDirectoryExistsAndClear($this->vendorDir);
 
-        $this->binDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'composer-test-bin';
+        $this->binDir = $this->rootDir.DIRECTORY_SEPARATOR.'bin';
         $this->ensureDirectoryExistsAndClear($this->binDir);
 
         $this->config->merge(array(
@@ -61,8 +63,7 @@ class LibraryInstallerTest extends TestCase
 
     protected function tearDown()
     {
-        $this->fs->removeDirectory($this->vendorDir);
-        $this->fs->removeDirectory($this->binDir);
+        $this->fs->removeDirectory($this->rootDir);
     }
 
     public function testInstallerCreationShouldNotCreateVendorDirectory()

+ 32 - 16
tests/Composer/Test/InstallerTest.php

@@ -26,7 +26,10 @@ use Composer\Test\Mock\InstalledFilesystemRepositoryMock;
 use Composer\Test\Mock\InstallationManagerMock;
 use Symfony\Component\Console\Input\StringInput;
 use Symfony\Component\Console\Output\StreamOutput;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Formatter\OutputFormatter;
 use Composer\TestCase;
+use Composer\IO\BufferIO;
 
 class InstallerTest extends TestCase
 {
@@ -137,7 +140,7 @@ class InstallerTest extends TestCase
     /**
      * @dataProvider getIntegrationTests
      */
-    public function testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectExitCode)
+    public function testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectResult)
     {
         if ($condition) {
             eval('$res = '.$condition.';');
@@ -146,18 +149,15 @@ class InstallerTest extends TestCase
             }
         }
 
-        $output = null;
-        $io = $this->getMock('Composer\IO\IOInterface');
-        $callback = function ($text, $newline) use (&$output) {
-            $output .= $text . ($newline ? "\n" : "");
-        };
-        $io->expects($this->any())
-            ->method('write')
-            ->will($this->returnCallback($callback));
-        $io->expects($this->any())
-            ->method('writeError')
-            ->will($this->returnCallback($callback));
+        $io = new BufferIO('', OutputInterface::VERBOSITY_NORMAL, new OutputFormatter(false));
+
+        // Prepare for exceptions
+        if (!is_int($expectResult)) {
+            $normalizedOutput = rtrim(str_replace("\n", PHP_EOL, $expect));
+            $this->setExpectedException($expectResult, $normalizedOutput);
+        }
 
+        // Create Composer mock object according to configuration
         $composer = FactoryMock::create($io, $composerConfig);
 
         $jsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock();
@@ -233,8 +233,14 @@ class InstallerTest extends TestCase
         $appOutput = fopen('php://memory', 'w+');
         $result = $application->run(new StringInput($run), new StreamOutput($appOutput));
         fseek($appOutput, 0);
-        $this->assertEquals($expectExitCode, $result, $output . stream_get_contents($appOutput));
 
+        // Shouldn't check output and results if an exception was expected by this point
+        if (!is_int($expectResult)) {
+            return;
+        }
+
+        $output = str_replace("\r", '', $io->getOutput());
+        $this->assertEquals($expectResult, $result, $output . stream_get_contents($appOutput));
         if ($expectLock) {
             unset($actualLock['hash']);
             unset($actualLock['content-hash']);
@@ -266,7 +272,7 @@ class InstallerTest extends TestCase
             $installedDev = array();
             $lock = array();
             $expectLock = array();
-            $expectExitCode = 0;
+            $expectResult = 0;
 
             try {
                 $message = $testData['TEST'];
@@ -303,12 +309,21 @@ class InstallerTest extends TestCase
                 }
                 $expectOutput = isset($testData['EXPECT-OUTPUT']) ? $testData['EXPECT-OUTPUT'] : null;
                 $expect = $testData['EXPECT'];
-                $expectExitCode = isset($testData['EXPECT-EXIT-CODE']) ? (int) $testData['EXPECT-EXIT-CODE'] : 0;
+                if (!empty($testData['EXPECT-EXCEPTION'])) {
+                    $expectResult = $testData['EXPECT-EXCEPTION'];
+                    if (!empty($testData['EXPECT-EXIT-CODE'])) {
+                        throw new \LogicException('EXPECT-EXCEPTION and EXPECT-EXIT-CODE are mutually exclusive');
+                    }
+                } elseif (!empty($testData['EXPECT-EXIT-CODE'])) {
+                    $expectResult = (int) $testData['EXPECT-EXIT-CODE'];
+                } else {
+                    $expectResult = 0;
+                }
             } catch (\Exception $e) {
                 die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file)));
             }
 
-            $tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectExitCode);
+            $tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectResult);
         }
 
         return $tests;
@@ -328,6 +343,7 @@ class InstallerTest extends TestCase
             'EXPECT-LOCK' => false,
             'EXPECT-OUTPUT' => false,
             'EXPECT-EXIT-CODE' => false,
+            'EXPECT-EXCEPTION' => false,
             'EXPECT' => true,
         );
 

+ 3 - 2
tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php

@@ -13,11 +13,12 @@
 namespace Composer\Test\Package\Archiver;
 
 use Composer\Package\Archiver\ArchivableFilesFinder;
+use Composer\TestCase;
 use Composer\Util\Filesystem;
 use Symfony\Component\Process\Process;
 use Symfony\Component\Process\ExecutableFinder;
 
-class ArchivableFilesFinderTest extends \PHPUnit_Framework_TestCase
+class ArchivableFilesFinderTest extends TestCase
 {
     protected $sources;
     protected $finder;
@@ -29,7 +30,7 @@ class ArchivableFilesFinderTest extends \PHPUnit_Framework_TestCase
         $this->fs = $fs;
 
         $this->sources = $fs->normalizePath(
-            realpath(sys_get_temp_dir()).'/composer_archiver_test'.uniqid(mt_rand(), true)
+            $this->getUniqueTmpDirectory()
         );
 
         $fileTree = array(

+ 3 - 3
tests/Composer/Test/Package/Archiver/ArchiverTest.php

@@ -12,11 +12,12 @@
 
 namespace Composer\Test\Package\Archiver;
 
+use Composer\TestCase;
 use Composer\Util\Filesystem;
 use Composer\Util\ProcessExecutor;
 use Composer\Package\Package;
 
-abstract class ArchiverTest extends \PHPUnit_Framework_TestCase
+abstract class ArchiverTest extends TestCase
 {
     /**
      * @var \Composer\Util\Filesystem
@@ -37,8 +38,7 @@ abstract class ArchiverTest extends \PHPUnit_Framework_TestCase
     {
         $this->filesystem = new Filesystem();
         $this->process    = new ProcessExecutor();
-        $this->testDir    = sys_get_temp_dir().'/composer_archiver_test_'.mt_rand();
-        $this->filesystem->ensureDirectoryExists($this->testDir);
+        $this->testDir    = $this->getUniqueTmpDirectory();
     }
 
     public function tearDown()

+ 4 - 4
tests/Composer/Test/Package/Archiver/PharArchiverTest.php

@@ -21,14 +21,14 @@ class PharArchiverTest extends ArchiverTest
         // Set up repository
         $this->setupDummyRepo();
         $package = $this->setupPackage();
-        $target  = sys_get_temp_dir().'/composer_archiver_test.tar';
+        $target  = $this->getUniqueTmpDirectory().'/composer_archiver_test.tar';
 
         // Test archive
         $archiver = new PharArchiver();
         $archiver->archive($package->getSourceUrl(), $target, 'tar', array('foo/bar', 'baz', '!/foo/bar/baz'));
         $this->assertFileExists($target);
 
-        unlink($target);
+        $this->filesystem->removeDirectory(dirname($target));
     }
 
     public function testZipArchive()
@@ -36,14 +36,14 @@ class PharArchiverTest extends ArchiverTest
         // Set up repository
         $this->setupDummyRepo();
         $package = $this->setupPackage();
-        $target  = sys_get_temp_dir().'/composer_archiver_test.zip';
+        $target  = $this->getUniqueTmpDirectory().'/composer_archiver_test.zip';
 
         // Test archive
         $archiver = new PharArchiver();
         $archiver->archive($package->getSourceUrl(), $target, 'zip');
         $this->assertFileExists($target);
 
-        unlink($target);
+        $this->filesystem->removeDirectory(dirname($target));
     }
 
     /**

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