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.
  ...
main
Rob Bast 9 years ago
commit be5719eb53

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

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

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

2
composer.lock generated

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save