1
0
Fork 0

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.
  ...
pull/4817/head
Rob Bast 2016-02-04 12:43:37 +01:00
commit be5719eb53
122 changed files with 2164 additions and 621 deletions

32
.php_cs
View File

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

View File

@ -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 --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 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 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 SPDX 2.0 support, and externalized that in a composer/spdx-licenses lib
* Added warnings when the classmap autoloader finds duplicate classes * Added warnings when the classmap autoloader finds duplicate classes
* Added --file to the `archive` command to choose the filename * Added --file to the `archive` command to choose the filename

View File

@ -45,7 +45,8 @@
} }
}, },
"suggest": { "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" "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages"
}, },
"autoload": { "autoload": {

2
composer.lock generated
View File

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

View File

@ -109,7 +109,7 @@ mv composer.phar /usr/local/bin/composer
A quick copy-paste version including sudo: A quick copy-paste version including sudo:
```sh ```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 > **Note:** On some versions of OSX the `/usr` directory does not exist by

View File

@ -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 you may have to run the command with `root` privileges
```sh ```sh
sudo composer self-update sudo -H composer self-update
``` ```
### Options ### 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 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 ## create-project
You can use Composer to create new projects from an existing package. This is 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 By setting this environmental value, you can set a path to a certificate bundle
file to be used during SSL/TLS peer verification. 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 ### COMPOSER_DISCARD_CHANGES
This env var controls the [`discard-changes`](06-config.md#discard-changes) config option. This env var controls the [`discard-changes`](06-config.md#discard-changes) config option.

View File

@ -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 local package will be symlinked if possible, in which case the output in
the console will read `Symlinked from ../../packages/my-package`. If symlinking 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 is _not_ possible the package will be copied. In that case, the console will

View File

@ -55,9 +55,15 @@ php_openssl extension in php.ini.
## cafile ## cafile
A way to set the path to the openssl CA file. In PHP 5.6+ you should rather Location of Certificate Authority file on local filesystem. In PHP 5.6+ you
set this via openssl.cafile in php.ini, although PHP 5.6+ should be able to should rather set this via openssl.cafile in php.ini, although PHP 5.6+ should
detect your system CA file automatically. 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 ## http-basic

View File

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

View File

@ -36,7 +36,7 @@ as a normal package's.
The current composer plugin API version is 1.0.0. 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): part omitted):
```json ```json
@ -89,9 +89,54 @@ Furthermore plugins may implement the
event handlers automatically registered with the `EventDispatcher` when the event handlers automatically registered with the `EventDispatcher` when the
plugin is loaded. 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
<?php <?php

View File

@ -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. ## 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] 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 (../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. 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 The simplest work-around to this limitation, is moving or duplicating the `repositories` definition into your root
composer.json. composer.json.
## I have locked a dependency to a specific commit but get unexpected results. ## 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 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: frequently overlooked:
> **Note:** While this is convenient at times, it should not be how you use > **Note:** While this is convenient at times, it should not be how you use

View File

@ -149,6 +149,10 @@
"type": "string", "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." "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": { "http-basic": {
"type": "object", "type": "object",
"description": "A hash of domain name => {\"username\": \"...\", \"password\": \"...\"}.", "description": "A hash of domain name => {\"username\": \"...\", \"password\": \"...\"}.",

View File

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

View File

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

View File

@ -12,6 +12,8 @@
namespace Composer\Command; namespace Composer\Command;
use Composer\Util\Platform;
use Composer\Util\Silencer;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
@ -142,7 +144,7 @@ EOT
? ($this->config->get('home') . '/config.json') ? ($this->config->get('home') . '/config.json')
: ($input->getOption('file') ?: trim(getenv('COMPOSER')) ?: 'composer.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'))) { if ($configFile === 'composer.json' && !file_exists($configFile) && realpath(getcwd()) === realpath($this->config->get('home'))) {
file_put_contents($configFile, "{\n}\n"); file_put_contents($configFile, "{\n}\n");
} }
@ -157,16 +159,16 @@ EOT
$this->authConfigFile = new JsonFile($authConfigFile, null, $io); $this->authConfigFile = new JsonFile($authConfigFile, null, $io);
$this->authConfigSource = new JsonConfigSource($this->authConfigFile, true); $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()) { if ($input->getOption('global') && !$this->configFile->exists()) {
touch($this->configFile->getPath()); touch($this->configFile->getPath());
$this->configFile->write(array('config' => new \ArrayObject)); $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()) { if ($input->getOption('global') && !$this->authConfigFile->exists()) {
touch($this->authConfigFile->getPath()); touch($this->authConfigFile->getPath());
$this->authConfigFile->write(array('http-basic' => new \ArrayObject, 'github-oauth' => new \ArrayObject, 'gitlab-oauth' => new \ArrayObject)); $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()) { if (!$this->configFile->exists()) {
@ -183,7 +185,7 @@ EOT
if ($input->getOption('editor')) { if ($input->getOption('editor')) {
$editor = escapeshellcmd(getenv('EDITOR')); $editor = escapeshellcmd(getenv('EDITOR'));
if (!$editor) { if (!$editor) {
if (defined('PHP_WINDOWS_VERSION_BUILD')) { if (Platform::isWindows()) {
$editor = 'notepad'; $editor = 'notepad';
} else { } else {
foreach (array('vim', 'vi', 'nano', 'pico', 'ed') as $candidate) { foreach (array('vim', 'vi', 'nano', 'pico', 'ed') as $candidate) {
@ -196,7 +198,7 @@ EOT
} }
$file = $input->getOption('auth') ? $this->authConfigFile->getPath() : $this->configFile->getPath(); $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; return 0;
} }
@ -331,7 +333,11 @@ EOT
'disable-tls' => array($booleanValidator, $booleanNormalizer), 'disable-tls' => array($booleanValidator, $booleanNormalizer),
'cafile' => array( 'cafile' => array(
function ($val) { return file_exists($val) && is_readable($val); }, 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), 'github-expose-hostname' => array($booleanValidator, $booleanNormalizer),
); );
@ -434,9 +440,18 @@ EOT
} }
if (1 === count($values)) { if (1 === count($values)) {
$bool = strtolower($values[0]); $value = strtolower($values[0]);
if (true === $booleanValidator($bool) && false === $booleanNormalizer($bool)) { if (true === $booleanValidator($value)) {
return $this->configSource->addRepository($matches[1], false); 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);
} }
} }

View File

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

View File

@ -132,7 +132,7 @@ EOT
} else { } else {
$matchText = ''; $matchText = '';
if ($input->getOption('match-constraint') !== '*') { 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>'); $io->writeError('<info>There is no installed package depending on "'.$needle.'"'.$matchText.'.</info>');
} }

View File

@ -22,6 +22,7 @@ use Composer\Util\ConfigValidator;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\RemoteFilesystem;
use Composer\Util\StreamContextFactory; use Composer\Util\StreamContextFactory;
use Composer\Util\Keys;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -133,6 +134,9 @@ EOT
$io->write('Checking disk free space: ', false); $io->write('Checking disk free space: ', false);
$this->outputResult($this->checkDiskSpace($config)); $this->outputResult($this->checkDiskSpace($config));
$io->write('Checking pubkeys: ', false);
$this->outputResult($this->checkPubKeys($config));
$io->write('Checking composer version: ', false); $io->write('Checking composer version: ', false);
$this->outputResult($this->checkVersion()); $this->outputResult($this->checkVersion());
@ -327,6 +331,35 @@ EOT
return true; 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() private function checkVersion()
{ {
$protocol = extension_loaded('openssl') ? 'https' : 'http'; $protocol = extension_loaded('openssl') ? 'https' : 'http';

View File

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

View File

@ -37,6 +37,7 @@ class RemoveCommand extends Command
->setDefinition(array( ->setDefinition(array(
new InputArgument('packages', InputArgument::IS_ARRAY, 'Packages that should be removed.'), 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('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-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('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.'), 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 // Update packages
$composer = $this->getComposer(); $composer = $this->getComposer(true, $input->getOption('no-plugins'));
$composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output);

View File

@ -42,6 +42,7 @@ class RequireCommand extends InitCommand
new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'), 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-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('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-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('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.'), 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(); $composerDefinition = $json->read();
$composerBackup = file_get_contents($json->getPath()); $composerBackup = file_get_contents($json->getPath());
$composer = $this->getComposer(); $composer = $this->getComposer(true, $input->getOption('no-plugins'));
$repos = $composer->getRepositoryManager()->getRepositories(); $repos = $composer->getRepositoryManager()->getRepositories();
$platformOverrides = $composer->getConfig()->get('platform') ?: array(); $platformOverrides = $composer->getConfig()->get('platform') ?: array();
@ -143,7 +144,7 @@ EOT
// Update packages // Update packages
$this->resetComposer(); $this->resetComposer();
$composer = $this->getComposer(); $composer = $this->getComposer(true, $input->getOption('no-plugins'));
$composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output);

View File

@ -14,8 +14,10 @@ namespace Composer\Command;
use Composer\Composer; use Composer\Composer;
use Composer\Factory; use Composer\Factory;
use Composer\Config;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\RemoteFilesystem; use Composer\Util\Keys;
use Composer\IO\IOInterface;
use Composer\Downloader\FilesystemException; use Composer\Downloader\FilesystemException;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; 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 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 InputArgument('version', InputArgument::OPTIONAL, 'The version to update to'),
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), 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 ->setHelp(<<<EOT
The <info>self-update</info> command checks getcomposer.org for newer The <info>self-update</info> command checks getcomposer.org for newer
@ -71,8 +74,13 @@ EOT
$cacheDir = $config->get('cache-dir'); $cacheDir = $config->get('cache-dir');
$rollbackDir = $config->get('data-dir'); $rollbackDir = $config->get('data-dir');
$home = $config->get('home');
$localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0]; $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 // check if current dir is writable and if not try the cache dir from settings
$tmpDir = is_writable(dirname($localFilename)) ? dirname($localFilename) : $cacheDir; $tmpDir = is_writable(dirname($localFilename)) ? dirname($localFilename) : $cacheDir;
@ -80,9 +88,6 @@ EOT
if (!is_writable($tmpDir)) { if (!is_writable($tmpDir)) {
throw new FilesystemException('Composer update failed: the "'.$tmpDir.'" directory used to download the temp file could not be written'); 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')) { if ($input->getOption('rollback')) {
return $this->rollback($output, $rollbackDir, $localFilename); return $this->rollback($output, $rollbackDir, $localFilename);
@ -112,15 +117,79 @@ EOT
self::OLD_INSTALL_EXT self::OLD_INSTALL_EXT
); );
$io->writeError(sprintf("Updating to version <info>%s</info>.", $updateVersion)); $updatingToTag = !preg_match('{^[0-9a-f]{40}$}', $updateVersion);
$remoteFilename = $baseUrl . (preg_match('{^[0-9a-f]{40}$}', $updateVersion) ? '/composer.phar' : "/download/{$updateVersion}/composer.phar");
$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')); $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>'); $io->writeError('<error>The download of the new composer version failed for an unexpected reason</error>');
return 1; 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 // remove saved installations of composer
if ($input->getOption('clean-backups')) { if ($input->getOption('clean-backups')) {
$finder = $this->getOldInstallationFinder($rollbackDir); $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) protected function rollback(OutputInterface $output, $rollbackDir, $localFilename)
{ {
$rollbackVersion = $this->getLastBackupVersion($rollbackDir); $rollbackVersion = $this->getLastBackupVersion($rollbackDir);
@ -154,10 +268,6 @@ EOT
throw new \UnexpectedValueException('Composer rollback failed: no installation to roll back to in "'.$rollbackDir.'"'); 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; $old = $rollbackDir . '/' . $rollbackVersion . self::OLD_INSTALL_EXT;
if (!is_file($old)) { if (!is_file($old)) {

View File

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

View File

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

View File

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

View File

@ -12,10 +12,11 @@
namespace Composer\Console; namespace Composer\Console;
use Composer\Util\Platform;
use Composer\Util\Silencer;
use Symfony\Component\Console\Application as BaseApplication; use Symfony\Component\Console\Application as BaseApplication;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Formatter\OutputFormatter; 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')) { 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) { if (!$shutdownRegistered) {
@ -136,9 +137,7 @@ class Application extends BaseApplication
if ($newWorkDir = $this->getNewWorkingDir($input)) { if ($newWorkDir = $this->getNewWorkingDir($input)) {
$oldWorkingDir = getcwd(); $oldWorkingDir = getcwd();
chdir($newWorkDir); chdir($newWorkDir);
if ($io->isDebug() >= 4) { $io->writeError('Changed CWD to ' . getcwd(), true, IOInterface::DEBUG);
$io->writeError('Changed CWD to ' . getcwd());
}
} }
// add non-standard scripts as own commands // add non-standard scripts as own commands
@ -203,30 +202,32 @@ class Application extends BaseApplication
{ {
$io = $this->getIO(); $io = $this->getIO();
Silencer::suppress();
try { try {
$composer = $this->getComposer(false, true); $composer = $this->getComposer(false, true);
if ($composer) { if ($composer) {
$config = $composer->getConfig(); $config = $composer->getConfig();
$minSpaceFree = 1024 * 1024; $minSpaceFree = 1024 * 1024;
if ((($df = @disk_free_space($dir = $config->get('home'))) !== 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 = $config->get('vendor-dir'))) !== false && $df < $minSpaceFree)
|| (($df = @disk_free_space($dir = sys_get_temp_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) { } catch (\Exception $e) {
} }
Silencer::restore();
if (defined('PHP_WINDOWS_VERSION_BUILD') && false !== strpos($exception->getMessage(), 'The system cannot find the path specified')) { 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>'); $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>'); $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')) { 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>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>'); $io->writeError('<error>Check https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors for details</error>', true, IOInterface::QUIET);
} }
} }

View File

@ -31,12 +31,21 @@ class SolverProblemsException extends \RuntimeException
protected function createMessage() protected function createMessage()
{ {
$text = "\n"; $text = "\n";
$hasExtensionProblems = false;
foreach ($this->problems as $i => $problem) { foreach ($this->problems as $i => $problem) {
$text .= " Problem ".($i + 1).$problem->getPrettyString($this->installedMap)."\n"; $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')) { 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; return $text;
@ -46,4 +55,40 @@ class SolverProblemsException extends \RuntimeException
{ {
return $this->problems; 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;
}
} }

View File

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

View File

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

View File

@ -14,6 +14,7 @@ namespace Composer\Downloader;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Util\Git as GitUtil; use Composer\Util\Git as GitUtil;
use Composer\Util\Platform;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
@ -43,7 +44,7 @@ class GitDownloader extends VcsDownloader
$path = $this->normalizePath($path); $path = $this->normalizePath($path);
$ref = $package->getSourceReference(); $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'; $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); $this->io->writeError(" Cloning ".$ref);
@ -353,7 +354,7 @@ class GitDownloader extends VcsDownloader
protected function normalizePath($path) protected function normalizePath($path)
{ {
if (defined('PHP_WINDOWS_VERSION_MAJOR') && strlen($path) > 0) { if (Platform::isWindows() && strlen($path) > 0) {
$basePath = $path; $basePath = $path;
$removed = array(); $removed = array();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,7 +35,7 @@ class BufferIO extends ConsoleIO
$input = new StringInput($input); $input = new StringInput($input);
$input->setInteractive(false); $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())); parent::__construct($input, $output, new HelperSet(array()));
} }

View File

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

View File

@ -21,6 +21,12 @@ use Composer\Config;
*/ */
interface IOInterface interface IOInterface
{ {
const QUIET = 1;
const NORMAL = 2;
const VERBOSE = 4;
const VERY_VERBOSE = 8;
const DEBUG = 16;
/** /**
* Is this input means interactive? * Is this input means interactive?
* *
@ -59,36 +65,40 @@ interface IOInterface
/** /**
* Writes a message to the output. * Writes a message to the output.
* *
* @param string|array $messages The message as an array of lines or a single string * @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 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. * Writes a message to the error output.
* *
* @param string|array $messages The message as an array of lines or a single string * @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 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. * Overwrites a previous message to the output.
* *
* @param string|array $messages The message as an array of lines or a single string * @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 bool $newline Whether to add a newline or not
* @param int $size The size of line * @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. * Overwrites a previous message to the error output.
* *
* @param string|array $messages The message as an array of lines or a single string * @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 bool $newline Whether to add a newline or not
* @param int $size The size of line * @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. * Asks a question to the user.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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->setAliases($aliases);
$realPackage->setStabilityFlags($stabilityFlags); $realPackage->setStabilityFlags($stabilityFlags);
$realPackage->setReferences($references); $realPackage->setReferences($references);

View File

@ -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
{
}

View File

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

View File

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

View File

@ -23,6 +23,7 @@ use Composer\Package\PackageInterface;
use Composer\Package\Link; use Composer\Package\Link;
use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\Constraint;
use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Pool;
use Composer\Plugin\Capability\Capability;
/** /**
* Plugin manager * Plugin manager
@ -122,8 +123,11 @@ class PluginManager
$currentPluginApiVersion = $this->getPluginApiVersion(); $currentPluginApiVersion = $this->getPluginApiVersion();
$currentPluginApiConstraint = new Constraint('==', $this->versionParser->normalize($currentPluginApiVersion)); $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>'); $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; return;
} }
} }
@ -202,9 +206,7 @@ class PluginManager
*/ */
private function addPlugin(PluginInterface $plugin) private function addPlugin(PluginInterface $plugin)
{ {
if ($this->io->isDebug()) { $this->io->writeError('Loading plugin '.get_class($plugin), true, IOInterface::DEBUG);
$this->io->writeError('Loading plugin '.get_class($plugin));
}
$this->plugins[] = $plugin; $this->plugins[] = $plugin;
$plugin->activate($this->composer, $this->io); $plugin->activate($this->composer, $this->io);
@ -299,4 +301,58 @@ class PluginManager
return $this->globalComposer->getInstallationManager()->getInstallPath($package); 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;
}
}
} }

View File

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

View File

@ -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->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; $this->degradedMode = true;
return true; return true;
} }
} }

View File

@ -113,7 +113,7 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn
parent::initialize(); parent::initialize();
foreach ($this->getUrlMatches() as $url) { foreach ($this->getUrlMatches() as $url) {
$path = realpath($url) . '/'; $path = realpath($url) . DIRECTORY_SEPARATOR;
$composerFilePath = $path.'composer.json'; $composerFilePath = $path.'composer.json';
if (!file_exists($composerFilePath)) { if (!file_exists($composerFilePath)) {
@ -125,16 +125,16 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn
$package['dist'] = array( $package['dist'] = array(
'type' => 'path', 'type' => 'path',
'url' => $url, 'url' => $url,
'reference' => '', 'reference' => sha1($json),
); );
if (!isset($package['version'])) { if (!isset($package['version'])) {
$package['version'] = $this->versionGuesser->guessVersion($package, $path) ?: 'dev-master'; $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); $package['dist']['reference'] = trim($output);
} else {
$package['dist']['reference'] = Locker::getContentHash($json);
} }
$package = $this->loader->load($package); $package = $this->loader->load($package);
@ -153,6 +153,9 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn
*/ */
private function getUrlMatches() 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));
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -110,7 +110,7 @@ class Filesystem
return $this->removeDirectoryPhp($directory); return $this->removeDirectoryPhp($directory);
} }
if (defined('PHP_WINDOWS_VERSION_BUILD')) { if (Platform::isWindows()) {
$cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape(realpath($directory))); $cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape(realpath($directory)));
} else { } else {
$cmd = sprintf('rm -rf %s', ProcessExecutor::escape($directory)); $cmd = sprintf('rm -rf %s', ProcessExecutor::escape($directory));
@ -181,10 +181,10 @@ class Filesystem
{ {
if (!@$this->unlinkImplementation($path)) { if (!@$this->unlinkImplementation($path)) {
// retry after a bit on windows since it tends to be touchy with mass removals // 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(); $error = error_get_last();
$message = 'Could not delete '.$path.': ' . @$error['message']; $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"; $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)) { if (!@rmdir($path)) {
// retry after a bit on windows since it tends to be touchy with mass removals // 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(); $error = error_get_last();
$message = 'Could not delete '.$path.': ' . @$error['message']; $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"; $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); 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. // 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)); $command = sprintf('xcopy %s %s /E /I /Q /Y', ProcessExecutor::escape($source), ProcessExecutor::escape($target));
$result = $this->processExecutor->execute($command, $output); $result = $this->processExecutor->execute($command, $output);
@ -460,7 +460,7 @@ class Filesystem
public static function getPlatformPath($path) public static function getPlatformPath($path)
{ {
if (defined('PHP_WINDOWS_VERSION_BUILD')) { if (Platform::isWindows()) {
$path = preg_replace('{^(?:file:///([a-z])/)}i', 'file://$1:/', $path); $path = preg_replace('{^(?:file:///([a-z])/)}i', 'file://$1:/', $path);
} }
@ -498,7 +498,7 @@ class Filesystem
*/ */
private function unlinkImplementation($path) 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); return rmdir($path);
} }

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@ class ProcessExecutor
// make sure that null translate to the proper directory in case the dir is a symlink // 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 // 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()); $cwd = realpath(getcwd());
} }

View File

@ -33,11 +33,14 @@ class RemoteFilesystem
private $progress; private $progress;
private $lastProgress; private $lastProgress;
private $options = array(); private $options = array();
private $peerCertificateMap = array();
private $disableTls = false; private $disableTls = false;
private $retryAuthFailure; private $retryAuthFailure;
private $lastHeaders; private $lastHeaders;
private $storeAuth; private $storeAuth;
private $degradedMode = false; private $degradedMode = false;
private $redirects;
private $maxRedirects = 20;
/** /**
* Constructor. * Constructor.
@ -54,15 +57,7 @@ class RemoteFilesystem
// Setup TLS options // Setup TLS options
// The cafile option can be set via config.json // The cafile option can be set via config.json
if ($disableTls === false) { if ($disableTls === false) {
$this->options = $this->getTlsDefaults(); $this->options = $this->getTlsDefaults($options);
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.');
}
} else { } else {
$this->disableTls = true; $this->disableTls = true;
} }
@ -139,8 +134,8 @@ class RemoteFilesystem
} }
/** /**
* @param array $headers array of returned headers like from getLastHeaders() * @param array $headers array of returned headers like from getLastHeaders()
* @param string $name header name (case insensitive) * @param string $name header name (case insensitive)
* @return string|null * @return string|null
*/ */
public function findHeaderValue(array $headers, $name) public 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 * @return int|null
*/ */
public function findStatusCode(array $headers) public function findStatusCode(array $headers)
@ -206,24 +201,34 @@ class RemoteFilesystem
$this->lastProgress = null; $this->lastProgress = null;
$this->retryAuthFailure = true; $this->retryAuthFailure = true;
$this->lastHeaders = array(); $this->lastHeaders = array();
$this->redirects = 1; // The first request counts.
// capture username/password from URL if there is one // capture username/password from URL if there is one
if (preg_match('{^https?://(.+):(.+)@([^/]+)}i', $fileUrl, $match)) { if (preg_match('{^https?://(.+):(.+)@([^/]+)}i', $fileUrl, $match)) {
$this->io->setAuthentication($originUrl, urldecode($match[1]), urldecode($match[2])); $this->io->setAuthentication($originUrl, urldecode($match[1]), urldecode($match[2]));
} }
if (isset($additionalOptions['retry-auth-failure'])) { $tempAdditionalOptions = $additionalOptions;
$this->retryAuthFailure = (bool) $additionalOptions['retry-auth-failure']; 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()) { unset($tempAdditionalOptions['redirects']);
$this->io->writeError((substr($fileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $fileUrl);
} }
$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'])) { if (isset($options['github-token'])) {
$fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['github-token']; $fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['github-token'];
unset($options['github-token']); unset($options['github-token']);
@ -245,7 +250,7 @@ class RemoteFilesystem
$ctx = StreamContextFactory::getContext($fileUrl, $options, array('notification' => array($this, 'callbackGet'))); $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); $this->io->writeError(" Downloading: <comment>Connecting...</comment>", false);
} }
@ -260,6 +265,18 @@ class RemoteFilesystem
}); });
try { try {
$result = file_get_contents($fileUrl, false, $ctx); $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) { } catch (\Exception $e) {
if ($e instanceof TransportException && !empty($http_response_header[0])) { if ($e instanceof TransportException && !empty($http_response_header[0])) {
$e->setHeaders($http_response_header); $e->setHeaders($http_response_header);
@ -293,6 +310,11 @@ class RemoteFilesystem
$statusCode = $this->findStatusCode($http_response_header); $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 // fail 4xx and 5xx responses and capture the response
if ($statusCode && $statusCode >= 400 && $statusCode <= 599) { if ($statusCode && $statusCode >= 400 && $statusCode <= 599) {
if (!$this->retry) { if (!$this->retry) {
@ -305,7 +327,7 @@ class RemoteFilesystem
$result = false; $result = false;
} }
if ($this->progress && !$this->retry) { if ($this->progress && !$this->retry && !$isRedirect) {
$this->io->overwriteError(" Downloading: <comment>100%</comment>"); $this->io->overwriteError(" Downloading: <comment>100%</comment>");
} }
@ -342,7 +364,7 @@ class RemoteFilesystem
} }
// handle copy command if download was successful // handle copy command if download was successful
if (false !== $result && null !== $fileName) { if (false !== $result && null !== $fileName && !$isRedirect) {
if ('' === $result) { if ('' === $result) {
throw new TransportException('"'.$this->fileUrl.'" appears broken, and returned an empty 200 response'); 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) { if ($this->retry) {
$this->retry = false; $this->retry = false;
$result = $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress); $result = $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress);
$authHelper = new AuthHelper($this->io, $this->config); if ($this->storeAuth && $this->config) {
$authHelper->storeAuth($this->originUrl, $this->storeAuth); $authHelper = new AuthHelper($this->io, $this->config);
$this->storeAuth = false; $authHelper->storeAuth($this->originUrl, $this->storeAuth);
$this->storeAuth = false;
}
return $result; return $result;
} }
@ -522,19 +580,42 @@ class RemoteFilesystem
$tlsOptions = array(); $tlsOptions = array();
// Setup remaining TLS options - the matching may need monitoring, esp. www vs none in CN // Setup remaining TLS options - the matching may need monitoring, esp. www vs none in CN
if ($this->disableTls === false && PHP_VERSION_ID < 50600) { if ($this->disableTls === false && PHP_VERSION_ID < 50600 && !stream_is_local($this->fileUrl)) {
if (!preg_match('{^https?://}', $this->fileUrl)) { $host = parse_url($this->fileUrl, PHP_URL_HOST);
$host = $originUrl;
} else {
$host = parse_url($this->fileUrl, PHP_URL_HOST);
}
if ($host === 'github.com' || $host === 'api.github.com') { if (PHP_VERSION_ID >= 50304) {
$host = '*.github.com'; // Must manually follow when setting CN_match because this causes all
// redirects to be validated against the same CN_match value.
$userlandFollow = true;
} else {
// 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';
}
} }
$tlsOptions['ssl']['CN_match'] = $host; $tlsOptions['ssl']['CN_match'] = $host;
$tlsOptions['ssl']['SNI_server_name'] = $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(); $headers = array();
@ -551,6 +632,10 @@ class RemoteFilesystem
$headers[] = 'Connection: close'; $headers[] = 'Connection: close';
} }
if (isset($userlandFollow)) {
$options['http']['follow_location'] = 0;
}
if ($this->io->hasAuthentication($originUrl)) { if ($this->io->hasAuthentication($originUrl)) {
$auth = $this->io->getAuthentication($originUrl); $auth = $this->io->getAuthentication($originUrl);
if ('github.com' === $originUrl && 'x-oauth-basic' === $auth['password']) { if ('github.com' === $originUrl && 'x-oauth-basic' === $auth['password']) {
@ -575,7 +660,55 @@ class RemoteFilesystem
return $options; 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( $ciphers = implode(':', array(
'ECDHE-RSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-GCM-SHA256',
@ -600,7 +733,7 @@ class RemoteFilesystem
'DHE-DSS-AES256-SHA', 'DHE-DSS-AES256-SHA',
'DHE-RSA-AES256-SHA', 'DHE-RSA-AES256-SHA',
'AES128-GCM-SHA256', 'AES128-GCM-SHA256',
'AES256-GCM-SHA384', 'AES256-GCM-SHA384',
'ECDHE-RSA-RC4-SHA', 'ECDHE-RSA-RC4-SHA',
'ECDHE-ECDSA-RC4-SHA', 'ECDHE-ECDSA-RC4-SHA',
'AES128', 'AES128',
@ -613,7 +746,7 @@ class RemoteFilesystem
'!DES', '!DES',
'!3DES', '!3DES',
'!MD5', '!MD5',
'!PSK' '!PSK',
)); ));
/** /**
@ -622,89 +755,96 @@ class RemoteFilesystem
* *
* cafile or capath can be overridden by passing in those options to constructor. * cafile or capath can be overridden by passing in those options to constructor.
*/ */
$options = array( $defaults = array(
'ssl' => array( 'ssl' => array(
'ciphers' => $ciphers, 'ciphers' => $ciphers,
'verify_peer' => true, 'verify_peer' => true,
'verify_depth' => 7, 'verify_depth' => 7,
'SNI_enabled' => true, '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 * Attempt to find a local cafile or throw an exception if none pre-set
* The user may go download one if this occurs. * 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(); $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 if (preg_match('{^phar://}', $result)) {
// to work around https://bugs.php.net/bug.php?id=64634 $hash = hash_file('sha256', $result);
$source = fopen($result, 'r'); $targetPath = rtrim(sys_get_temp_dir(), '\\/') . '/composer-cacert-' . $hash . '.pem';
$target = fopen($targetPath, 'w+');
stream_copy_to_stream($source, $target);
fclose($source);
fclose($target);
unset($source, $target);
$options['ssl']['cafile'] = $targetPath; if (!file_exists($targetPath) || $hash !== hash_file('sha256', $targetPath)) {
} else { $this->streamCopy($result, $targetPath);
if (is_dir($result)) { chmod($targetPath, 0666);
$options['ssl']['capath'] = $result;
} elseif ($result) {
$options['ssl']['cafile'] = $result;
}
} }
$defaults['ssl']['cafile'] = $targetPath;
} elseif (is_dir($result)) {
$defaults['ssl']['capath'] = $result;
} else { } 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. * Disable TLS compression to prevent CRIME attacks where supported.
*/ */
if (PHP_VERSION_ID >= 50413) { 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. * This method was adapted from Sslurp.
* https://github.com/EvanDotPro/Sslurp * https://github.com/EvanDotPro/Sslurp
* *
* (c) Evan Coury <me@evancoury.com> * (c) Evan Coury <me@evancoury.com>
* *
* For the full copyright and license information, please see below: * For the full copyright and license information, please see below:
* *
* Copyright (c) 2013, Evan Coury * Copyright (c) 2013, Evan Coury
* All rights reserved. * All rights reserved.
* *
* Redistribution and use in source and binary forms, with or without modification, * Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met: * are permitted provided that the following conditions are met:
* *
* * Redistributions of source code must retain the above copyright notice, * * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer. * this list of conditions and the following disclaimer.
* *
* * Redistributions in binary form must reproduce the above copyright notice, * * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation * this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution. * and/or other materials provided with the distribution.
* *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * 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 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * 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 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ *
* @return string
*/
private function getSystemCaRootBundlePath() private function getSystemCaRootBundlePath()
{ {
static $caPath = null; static $caPath = null;
@ -721,6 +861,11 @@ class RemoteFilesystem
return $caPath = $envCertFile; return $caPath = $envCertFile;
} }
$configured = ini_get('openssl.cafile');
if ($configured && strlen($configured) > 0 && is_readable($configured) && $this->validateCaFile($configured)) {
return $caPath = $configured;
}
$caBundlePaths = array( $caBundlePaths = array(
'/etc/pki/tls/certs/ca-bundle.crt', // Fedora, RHEL, CentOS (ca-certificates package) '/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) '/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? '/usr/share/ssl/certs/ca-bundle.crt', // Really old RedHat?
'/etc/ssl/cert.pem', // OpenBSD '/etc/ssl/cert.pem', // OpenBSD
'/usr/local/etc/ssl/cert.pem', // FreeBSD 10.x '/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) { foreach ($caBundlePaths as $caBundle) {
if (@is_readable($caBundle) && $this->validateCaFile($caBundle)) { if (Silencer::call('is_readable', $caBundle) && $this->validateCaFile($caBundle)) {
return $caPath = $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) private function validateCaFile($filename)
{ {
if ($this->io->isDebug()) { static $files = array();
$this->io->writeError('Checking CA file '.realpath($filename));
if (isset($files[$filename])) {
return $files[$filename];
} }
$this->io->writeError('Checking CA file '.realpath($filename), true, IOInterface::DEBUG);
$contents = file_get_contents($filename); $contents = file_get_contents($filename);
// assume the CA is valid if php is vulnerable to // 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 // https://www.sektioneins.de/advisories/advisory-012013-php-openssl_x509_parse-memory-corruption-vulnerability.html
if ( if (!TlsHelper::isOpensslParseSafe()) {
PHP_VERSION_ID <= 50327 $this->io->writeError(sprintf(
|| (PHP_VERSION_ID >= 50400 && PHP_VERSION_ID < 50422) '<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_ID >= 50500 && PHP_VERSION_ID < 50506) PHP_VERSION
) { ));
return !empty($contents);
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;
} }
} }

View File

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

View File

@ -0,0 +1,289 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Util;
use Symfony\Component\Process\PhpProcess;
/**
* @author Chris Smith <chris@cs278.org>
*/
final class TlsHelper
{
private static $useOpensslParse;
/**
* Match hostname against a certificate.
*
* @param mixed $certificate X.509 certificate
* @param string $hostname Hostname in the URL
* @param string $cn Set to the common name of the certificate iff match found
*
* @return bool
*/
public static function checkCertificateHost($certificate, $hostname, &$cn = null)
{
$names = self::getCertificateNames($certificate);
if (empty($names)) {
return false;
}
$combinedNames = array_merge($names['san'], array($names['cn']));
$hostname = strtolower($hostname);
foreach ($combinedNames as $certName) {
$matcher = self::certNameMatcher($certName);
if ($matcher && $matcher($hostname)) {
$cn = $names['cn'];
return true;
}
}
return false;
}
/**
* Extract DNS names out of an X.509 certificate.
*
* @param mixed $certificate X.509 certificate
*
* @return array|null
*/
public static function getCertificateNames($certificate)
{
if (is_array($certificate)) {
$info = $certificate;
} elseif (self::isOpensslParseSafe()) {
$info = openssl_x509_parse($certificate, false);
}
if (!isset($info['subject']['commonName'])) {
return;
}
$commonName = strtolower($info['subject']['commonName']);
$subjectAltNames = array();
if (isset($info['extensions']['subjectAltName'])) {
$subjectAltNames = preg_split('{\s*,\s*}', $info['extensions']['subjectAltName']);
$subjectAltNames = array_filter(array_map(function ($name) {
if (0 === strpos($name, 'DNS:')) {
return strtolower(ltrim(substr($name, 4)));
}
}, $subjectAltNames));
$subjectAltNames = array_values($subjectAltNames);
}
return array(
'cn' => $commonName,
'san' => $subjectAltNames,
);
}
/**
* Get the certificate pin.
*
* By Kevin McArthur of StormTide Digital Studios Inc.
* @KevinSMcArthur / https://github.com/StormTide
*
* See http://tools.ietf.org/html/draft-ietf-websec-key-pinning-02
*
* 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.
*/
public static function getCertificateFingerprint($certificate)
{
$pubkeydetails = openssl_pkey_get_details(openssl_get_publickey($certificate));
$pubkeypem = $pubkeydetails['key'];
//Convert PEM to DER before SHA1'ing
$start = '-----BEGIN PUBLIC KEY-----';
$end = '-----END PUBLIC KEY-----';
$pemtrim = substr($pubkeypem, (strpos($pubkeypem, $start) + strlen($start)), (strlen($pubkeypem) - strpos($pubkeypem, $end)) * (-1));
$der = base64_decode($pemtrim);
return sha1($der);
}
/**
* Test if it is safe to use the PHP function openssl_x509_parse().
*
* This checks if OpenSSL extensions is vulnerable to remote code execution
* via the exploit documented as CVE-2013-6420.
*
* @return bool
*/
public static function isOpensslParseSafe()
{
if (null !== self::$useOpensslParse) {
return self::$useOpensslParse;
}
if (PHP_VERSION_ID >= 50600) {
return self::$useOpensslParse = true;
}
// Vulnerable:
// PHP 5.3.0 - PHP 5.3.27
// PHP 5.4.0 - PHP 5.4.22
// PHP 5.5.0 - PHP 5.5.6
if (
(PHP_VERSION_ID < 50400 && PHP_VERSION_ID >= 50328)
|| (PHP_VERSION_ID < 50500 && PHP_VERSION_ID >= 50423)
|| (PHP_VERSION_ID < 50600 && PHP_VERSION_ID >= 50507)
) {
// This version of PHP has the fix for CVE-2013-6420 applied.
return self::$useOpensslParse = true;
}
if (Platform::isWindows()) {
// Windows is probably insecure in this case.
return self::$useOpensslParse = false;
}
$compareDistroVersionPrefix = function ($prefix, $fixedVersion) {
$regex = '{^'.preg_quote($prefix).'([0-9]+)$}';
if (preg_match($regex, PHP_VERSION, $m)) {
return ((int) $m[1]) >= $fixedVersion;
}
return false;
};
// Hard coded list of PHP distributions with the fix backported.
if (
$compareDistroVersionPrefix('5.3.3-7+squeeze', 18) // Debian 6 (Squeeze)
|| $compareDistroVersionPrefix('5.4.4-14+deb7u', 7) // Debian 7 (Wheezy)
|| $compareDistroVersionPrefix('5.3.10-1ubuntu3.', 9) // Ubuntu 12.04 (Precise)
) {
return self::$useOpensslParse = true;
}
// This is where things get crazy, because distros backport security
// fixes the chances are on NIX systems the fix has been applied but
// it's not possible to verify that from the PHP version.
//
// To verify exec a new PHP process and run the issue testcase with
// known safe input that replicates the bug.
// Based on testcase in https://github.com/php/php-src/commit/c1224573c773b6845e83505f717fbf820fc18415
// changes in https://github.com/php/php-src/commit/76a7fd893b7d6101300cc656058704a73254d593
$cert = 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVwRENDQTR5Z0F3SUJBZ0lKQUp6dThyNnU2ZUJjTUEwR0NTcUdTSWIzRFFFQkJRVUFNSUhETVFzd0NRWUQKVlFRR0V3SkVSVEVjTUJvR0ExVUVDQXdUVG05eVpISm9aV2x1TFZkbGMzUm1ZV3hsYmpFUU1BNEdBMVVFQnd3SApTOE9Ed3Jac2JqRVVNQklHQTFVRUNnd0xVMlZyZEdsdmJrVnBibk14SHpBZEJnTlZCQXNNRmsxaGJHbGphVzkxCmN5QkRaWEowSUZObFkzUnBiMjR4SVRBZkJnTlZCQU1NR0cxaGJHbGphVzkxY3k1elpXdDBhVzl1WldsdWN5NWsKWlRFcU1DZ0dDU3FHU0liM0RRRUpBUlliYzNSbFptRnVMbVZ6YzJWeVFITmxhM1JwYjI1bGFXNXpMbVJsTUhVWQpaREU1TnpBd01UQXhNREF3TURBd1dnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBCkFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEKQUFBQUFBQVhEVEUwTVRFeU9ERXhNemt6TlZvd2djTXhDekFKQmdOVkJBWVRBa1JGTVJ3d0dnWURWUVFJREJOTwpiM0prY21obGFXNHRWMlZ6ZEdaaGJHVnVNUkF3RGdZRFZRUUhEQWRMdzRQQ3RteHVNUlF3RWdZRFZRUUtEQXRUClpXdDBhVzl1UldsdWN6RWZNQjBHQTFVRUN3d1dUV0ZzYVdOcGIzVnpJRU5sY25RZ1UyVmpkR2x2YmpFaE1COEcKQTFVRUF3d1liV0ZzYVdOcGIzVnpMbk5sYTNScGIyNWxhVzV6TG1SbE1Tb3dLQVlKS29aSWh2Y05BUWtCRmh0egpkR1ZtWVc0dVpYTnpaWEpBYzJWcmRHbHZibVZwYm5NdVpHVXdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCCkR3QXdnZ0VLQW9JQkFRRERBZjNobDdKWTBYY0ZuaXlFSnBTU0RxbjBPcUJyNlFQNjV1c0pQUnQvOFBhRG9xQnUKd0VZVC9OYSs2ZnNnUGpDMHVLOURaZ1dnMnRIV1dvYW5TYmxBTW96NVBINlorUzRTSFJaN2UyZERJalBqZGhqaAowbUxnMlVNTzV5cDBWNzk3R2dzOWxOdDZKUmZIODFNTjJvYlhXczROdHp0TE11RDZlZ3FwcjhkRGJyMzRhT3M4CnBrZHVpNVVhd1Raa3N5NXBMUEhxNWNNaEZHbTA2djY1Q0xvMFYyUGQ5K0tBb2tQclBjTjVLTEtlYno3bUxwazYKU01lRVhPS1A0aWRFcXh5UTdPN2ZCdUhNZWRzUWh1K3ByWTNzaTNCVXlLZlF0UDVDWm5YMmJwMHdLSHhYMTJEWAoxbmZGSXQ5RGJHdkhUY3lPdU4rblpMUEJtM3ZXeG50eUlJdlZBZ01CQUFHalFqQkFNQWtHQTFVZEV3UUNNQUF3CkVRWUpZSVpJQVliNFFnRUJCQVFEQWdlQU1Bc0dBMVVkRHdRRUF3SUZvREFUQmdOVkhTVUVEREFLQmdnckJnRUYKQlFjREFqQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFHMGZaWVlDVGJkajFYWWMrMVNub2FQUit2SThDOENhRAo4KzBVWWhkbnlVNGdnYTBCQWNEclk5ZTk0ZUVBdTZacXljRjZGakxxWFhkQWJvcHBXb2NyNlQ2R0QxeDMzQ2tsClZBcnpHL0t4UW9oR0QySmVxa2hJTWxEb214SE83a2EzOStPYThpMnZXTFZ5alU4QVp2V01BcnVIYTRFRU55RzcKbFcyQWFnYUZLRkNyOVRuWFRmcmR4R1ZFYnY3S1ZRNmJkaGc1cDVTanBXSDErTXEwM3VSM1pYUEJZZHlWODMxOQpvMGxWajFLRkkyRENML2xpV2lzSlJvb2YrMWNSMzVDdGQwd1lCY3BCNlRac2xNY09QbDc2ZHdLd0pnZUpvMlFnClpzZm1jMnZDMS9xT2xOdU5xLzBUenprVkd2OEVUVDNDZ2FVK1VYZTRYT1Z2a2NjZWJKbjJkZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K';
$script = <<<'EOT'
error_reporting(-1);
$info = openssl_x509_parse(base64_decode('%s'));
var_dump(PHP_VERSION, $info['issuer']['emailAddress'], $info['validFrom_time_t']);
EOT;
$script = '<'."?php\n".sprintf($script, $cert);
try {
$process = new PhpProcess($script);
$process->mustRun();
} catch (\Exception $e) {
// In the case of any exceptions just accept it is not possible to
// determine the safety of openssl_x509_parse and bail out.
return self::$useOpensslParse = false;
}
$output = preg_split('{\r?\n}', trim($process->getOutput()));
$errorOutput = trim($process->getErrorOutput());
if (
count($output) === 3
&& $output[0] === sprintf('string(%d) "%s"', strlen(PHP_VERSION), PHP_VERSION)
&& $output[1] === 'string(27) "stefan.esser@sektioneins.de"'
&& $output[2] === 'int(-1)'
&& preg_match('{openssl_x509_parse\(\): illegal (?:ASN1 data type for|length in) timestamp in - on line \d+}', $errorOutput)
) {
// This PHP has the fix backported probably by a distro security team.
return self::$useOpensslParse = true;
}
return self::$useOpensslParse = false;
}
/**
* Convert certificate name into matching function.
*
* @param $certName CN/SAN
*
* @return callable|null
*/
private static function certNameMatcher($certName)
{
$wildcards = substr_count($certName, '*');
if (0 === $wildcards) {
// Literal match.
return function ($hostname) use ($certName) {
return $hostname === $certName;
};
}
if (1 === $wildcards) {
$components = explode('.', $certName);
if (3 > count($components)) {
// Must have 3+ components
return;
}
$firstComponent = $components[0];
// Wildcard must be the last character.
if ('*' !== $firstComponent[strlen($firstComponent) - 1]) {
return;
}
$wildcardRegex = preg_quote($certName);
$wildcardRegex = str_replace('\\*', '[a-z0-9-]+', $wildcardRegex);
$wildcardRegex = "{^{$wildcardRegex}$}";
return function ($hostname) use ($wildcardRegex) {
return 1 === preg_match($wildcardRegex, $hostname);
};
}
}
}

View File

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

View File

@ -14,6 +14,7 @@ namespace Composer\Test;
use Composer\Console\Application; use Composer\Console\Application;
use Composer\TestCase; use Composer\TestCase;
use Symfony\Component\Console\Output\OutputInterface;
class ApplicationTest extends TestCase class ApplicationTest extends TestCase
{ {
@ -30,11 +31,19 @@ class ApplicationTest extends TestCase
$index = 0; $index = 0;
if (extension_loaded('xdebug')) { if (extension_loaded('xdebug')) {
$outputMock->expects($this->at($index++))
->method("getVerbosity")
->willReturn(OutputInterface::VERBOSITY_NORMAL);
$outputMock->expects($this->at($index++)) $outputMock->expects($this->at($index++))
->method("write") ->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>')); ->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++)) $outputMock->expects($this->at($index++))
->method("write") ->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']))); ->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'])));

View File

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

View File

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

View File

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

View File

@ -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"
}
}
}
}
}

View File

@ -14,9 +14,10 @@ namespace Composer\Test\Json;
use Composer\Config\JsonConfigSource; use Composer\Config\JsonConfigSource;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\TestCase;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
class JsonConfigSourceTest extends \PHPUnit_Framework_TestCase class JsonConfigSourceTest extends TestCase
{ {
/** @var Filesystem */ /** @var Filesystem */
private $fs; private $fs;
@ -31,8 +32,7 @@ class JsonConfigSourceTest extends \PHPUnit_Framework_TestCase
protected function setUp() protected function setUp()
{ {
$this->fs = new Filesystem; $this->fs = new Filesystem;
$this->workingDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest'; $this->workingDir = $this->getUniqueTmpDirectory();
$this->fs->ensureDirectoryExists($this->workingDir);
} }
protected function tearDown() protected function tearDown()
@ -52,6 +52,24 @@ class JsonConfigSourceTest extends \PHPUnit_Framework_TestCase
$this->assertFileEquals($this->fixturePath('config/config-with-exampletld-repository.json'), $config); $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() public function testRemoveRepository()
{ {
$config = $this->workingDir.'/composer.json'; $config = $this->workingDir.'/composer.json';

View File

@ -148,6 +148,16 @@ class ConfigTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('/baz', $config->get('cache-dir')); $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() public function testFetchingRelativePaths()
{ {
$config = new Config(false, '/foo/bar'); $config = new Config(false, '/foo/bar');

View File

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

View File

@ -709,7 +709,7 @@ class SolverTest extends TestCase
$msg .= "Potential causes:\n"; $msg .= "Potential causes:\n";
$msg .= " - A typo in the package name\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 .= " - 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."; $msg .= "Read <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.";
$this->assertEquals($msg, $e->getMessage()); $this->assertEquals($msg, $e->getMessage());
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,9 +15,11 @@ namespace Composer\Test\EventDispatcher;
use Composer\EventDispatcher\Event; use Composer\EventDispatcher\Event;
use Composer\Installer\InstallerEvents; use Composer\Installer\InstallerEvents;
use Composer\TestCase; use Composer\TestCase;
use Composer\IO\BufferIO;
use Composer\Script\ScriptEvents; use Composer\Script\ScriptEvents;
use Composer\Script\CommandEvent; use Composer\Script\CommandEvent;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Symfony\Component\Console\Output\OutputInterface;
class EventDispatcherTest extends TestCase class EventDispatcherTest extends TestCase
{ {
@ -101,7 +103,7 @@ class EventDispatcherTest extends TestCase
$dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
->setConstructorArgs(array( ->setConstructorArgs(array(
$this->getMock('Composer\Composer'), $this->getMock('Composer\Composer'),
$io = $this->getMock('Composer\IO\IOInterface'), $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE),
$process, $process,
)) ))
->setMethods(array( ->setMethods(array(
@ -123,23 +125,12 @@ class EventDispatcherTest extends TestCase
->method('getListeners') ->method('getListeners')
->will($this->returnValue($listeners)); ->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); $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() public function testDispatcherCanExecuteComposerScriptGroups()
@ -148,7 +139,7 @@ class EventDispatcherTest extends TestCase
$dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
->setConstructorArgs(array( ->setConstructorArgs(array(
$composer = $this->getMock('Composer\Composer'), $composer = $this->getMock('Composer\Composer'),
$io = $this->getMock('Composer\IO\IOInterface'), $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE),
$process, $process,
)) ))
->setMethods(array( ->setMethods(array(
@ -174,31 +165,13 @@ class EventDispatcherTest extends TestCase
return array(); 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)); $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());
} }
/** /**

View File

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

View File

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

View File

@ -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?

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,6 +22,7 @@ class LibraryInstallerTest extends TestCase
{ {
protected $composer; protected $composer;
protected $config; protected $config;
protected $rootDir;
protected $vendorDir; protected $vendorDir;
protected $binDir; protected $binDir;
protected $dm; protected $dm;
@ -37,10 +38,11 @@ class LibraryInstallerTest extends TestCase
$this->config = new Config(); $this->config = new Config();
$this->composer->setConfig($this->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->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->ensureDirectoryExistsAndClear($this->binDir);
$this->config->merge(array( $this->config->merge(array(
@ -61,8 +63,7 @@ class LibraryInstallerTest extends TestCase
protected function tearDown() protected function tearDown()
{ {
$this->fs->removeDirectory($this->vendorDir); $this->fs->removeDirectory($this->rootDir);
$this->fs->removeDirectory($this->binDir);
} }
public function testInstallerCreationShouldNotCreateVendorDirectory() public function testInstallerCreationShouldNotCreateVendorDirectory()

View File

@ -26,7 +26,10 @@ use Composer\Test\Mock\InstalledFilesystemRepositoryMock;
use Composer\Test\Mock\InstallationManagerMock; use Composer\Test\Mock\InstallationManagerMock;
use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Console\Output\StreamOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Composer\TestCase; use Composer\TestCase;
use Composer\IO\BufferIO;
class InstallerTest extends TestCase class InstallerTest extends TestCase
{ {
@ -137,7 +140,7 @@ class InstallerTest extends TestCase
/** /**
* @dataProvider getIntegrationTests * @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) { if ($condition) {
eval('$res = '.$condition.';'); eval('$res = '.$condition.';');
@ -146,18 +149,15 @@ class InstallerTest extends TestCase
} }
} }
$output = null; $io = new BufferIO('', OutputInterface::VERBOSITY_NORMAL, new OutputFormatter(false));
$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));
// 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); $composer = FactoryMock::create($io, $composerConfig);
$jsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); $jsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock();
@ -233,8 +233,14 @@ class InstallerTest extends TestCase
$appOutput = fopen('php://memory', 'w+'); $appOutput = fopen('php://memory', 'w+');
$result = $application->run(new StringInput($run), new StreamOutput($appOutput)); $result = $application->run(new StringInput($run), new StreamOutput($appOutput));
fseek($appOutput, 0); 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) { if ($expectLock) {
unset($actualLock['hash']); unset($actualLock['hash']);
unset($actualLock['content-hash']); unset($actualLock['content-hash']);
@ -266,7 +272,7 @@ class InstallerTest extends TestCase
$installedDev = array(); $installedDev = array();
$lock = array(); $lock = array();
$expectLock = array(); $expectLock = array();
$expectExitCode = 0; $expectResult = 0;
try { try {
$message = $testData['TEST']; $message = $testData['TEST'];
@ -303,12 +309,21 @@ class InstallerTest extends TestCase
} }
$expectOutput = isset($testData['EXPECT-OUTPUT']) ? $testData['EXPECT-OUTPUT'] : null; $expectOutput = isset($testData['EXPECT-OUTPUT']) ? $testData['EXPECT-OUTPUT'] : null;
$expect = $testData['EXPECT']; $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) { } catch (\Exception $e) {
die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file))); 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; return $tests;
@ -328,6 +343,7 @@ class InstallerTest extends TestCase
'EXPECT-LOCK' => false, 'EXPECT-LOCK' => false,
'EXPECT-OUTPUT' => false, 'EXPECT-OUTPUT' => false,
'EXPECT-EXIT-CODE' => false, 'EXPECT-EXIT-CODE' => false,
'EXPECT-EXCEPTION' => false,
'EXPECT' => true, 'EXPECT' => true,
); );

View File

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

View File

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

View File

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

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