diff --git a/.php_cs b/.php_cs
index a2bd217ed..b9f3876ef 100644
--- a/.php_cs
+++ b/.php_cs
@@ -10,7 +10,7 @@ For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
EOF;
-$finder = Symfony\CS\Finder\DefaultFinder::create()
+$finder = Symfony\CS\Finder::create()
->files()
->name('*.php')
->exclude('Fixtures')
@@ -18,23 +18,27 @@ $finder = Symfony\CS\Finder\DefaultFinder::create()
->in(__DIR__.'/tests')
;
-return Symfony\CS\Config\Config::create()
+return Symfony\CS\Config::create()
->setUsingCache(true)
->setRiskyAllowed(true)
->setRules(array(
'@PSR2' => true,
- 'duplicate_semicolon' => true,
- 'extra_empty_lines' => true,
+ 'binary_operator_spaces' => true,
+ 'blank_line_before_return' => true,
'header_comment' => array('header' => $header),
'include' => true,
'long_array_syntax' => true,
'method_separation' => true,
- 'multiline_array_trailing_comma' => true,
- 'namespace_no_leading_whitespace' => true,
'no_blank_lines_after_class_opening' => true,
- 'no_empty_lines_after_phpdocs' => true,
- 'object_operator' => true,
- 'operators_spaces' => true,
+ 'no_blank_lines_after_phpdoc' => true,
+ 'no_blank_lines_between_uses' => true,
+ 'no_duplicate_semicolons' => true,
+ 'no_extra_consecutive_blank_lines' => true,
+ 'no_leading_import_slash' => true,
+ 'no_leading_namespace_whitespace' => true,
+ 'no_trailing_comma_in_singleline_array' => true,
+ 'no_unused_imports' => true,
+ 'object_operator_without_whitespace' => true,
'phpdoc_align' => true,
'phpdoc_indent' => true,
'phpdoc_no_access' => true,
@@ -44,15 +48,11 @@ return Symfony\CS\Config\Config::create()
'phpdoc_trim' => true,
'phpdoc_type_to_var' => true,
'psr0' => true,
- 'return' => true,
- 'remove_leading_slash_use' => true,
- 'remove_lines_between_uses' => true,
- 'single_array_no_trailing_comma' => true,
'single_blank_line_before_namespace' => true,
'spaces_cast' => true,
- 'standardize_not_equal' => true,
- 'ternary_spaces' => true,
- 'unused_use' => true,
+ 'standardize_not_equals' => true,
+ 'ternary_operator_spaces' => true,
+ 'trailing_comma_in_multiline_array' => true,
'whitespacy_lines' => true,
))
->finder($finder)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65e7018f6..6700114fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,7 +12,7 @@
* Added --strict to the `validate` command to treat any warning as an error that then returns a non-zero exit code
* Added a dependency on composer/semver, which is the externalized lib for all the version constraints parsing and handling
* Added support for classmap autoloading to load plugin classes and script handlers
- * Added `bin-compat` config option that if set to `full` will create .bat proxy for binaries even if Compoesr runs in a linux VM
+ * Added `bin-compat` config option that if set to `full` will create .bat proxy for binaries even if Composer runs in a linux VM
* Added SPDX 2.0 support, and externalized that in a composer/spdx-licenses lib
* Added warnings when the classmap autoloader finds duplicate classes
* Added --file to the `archive` command to choose the filename
diff --git a/composer.json b/composer.json
index 8fcd1ea93..f45775853 100644
--- a/composer.json
+++ b/composer.json
@@ -45,7 +45,8 @@
}
},
"suggest": {
- "ext-zip": "Enabling the zip extension allows you to unzip archives, and allows gzip compression of all internet traffic",
+ "ext-zip": "Enabling the zip extension allows you to unzip archives",
+ "ext-zlib": "Allow gzip compression of HTTP requests",
"ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages"
},
"autoload": {
diff --git a/composer.lock b/composer.lock
index caeba6397..31543e65c 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
- "hash": "fdf4b487fa59607376721ebec4ff4783",
+ "hash": "31b3c13c89f8d6c810637ca1fe8fc6ae",
"content-hash": "454148e20b837d9755dee7862f9c7a5d",
"packages": [
{
diff --git a/doc/00-intro.md b/doc/00-intro.md
index 1d09f2339..872bdff2c 100644
--- a/doc/00-intro.md
+++ b/doc/00-intro.md
@@ -109,7 +109,7 @@ mv composer.phar /usr/local/bin/composer
A quick copy-paste version including sudo:
```sh
-curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer
+curl -sS https://getcomposer.org/installer | sudo -H php -- --install-dir=/usr/local/bin --filename=composer
```
> **Note:** On some versions of OSX the `/usr` directory does not exist by
diff --git a/doc/03-cli.md b/doc/03-cli.md
index 8c855a286..b35d8f805 100644
--- a/doc/03-cli.md
+++ b/doc/03-cli.md
@@ -413,7 +413,7 @@ If you have installed Composer for your entire system (see [global installation]
you may have to run the command with `root` privileges
```sh
-sudo composer self-update
+sudo -H composer self-update
```
### Options
@@ -466,6 +466,12 @@ changes to the repositories section by using it the following way:
php composer.phar config repositories.foo vcs https://github.com/foo/bar
```
+If your repository requires more configuration options, you can instead pass its JSON representation :
+
+```sh
+php composer.phar config repositories.foo '{"type": "vcs", "url": "http://svn.example.org/my-project/", "trunk-path": "master"}'
+```
+
## create-project
You can use Composer to create new projects from an existing package. This is
@@ -711,6 +717,13 @@ commands) to finish executing. The default value is 300 seconds (5 minutes).
By setting this environmental value, you can set a path to a certificate bundle
file to be used during SSL/TLS peer verification.
+### COMPOSER_AUTH
+
+The `COMPOSER_AUTH` var allows you to set up authentication as an environment variable.
+The contents of the variable should be a JSON formatted object containing http-basic,
+github-oauth, ... objects as needed, and following the
+[spec from the config](06-config.md#gitlab-oauth).
+
### COMPOSER_DISCARD_CHANGES
This env var controls the [`discard-changes`](06-config.md#discard-changes) config option.
diff --git a/doc/05-repositories.md b/doc/05-repositories.md
index 7541f2a22..7bd3d1fe8 100644
--- a/doc/05-repositories.md
+++ b/doc/05-repositories.md
@@ -639,6 +639,11 @@ file, you can use the following configuration:
}
```
+If the package is a local VCS repository, the version may be inferred by
+the branch or tag that is currently checked out. Otherwise, the version should
+be explicitly defined in the package's `composer.json` file. If the version
+cannot be resolved by these means, it is assumed to be `dev-master`.
+
The local package will be symlinked if possible, in which case the output in
the console will read `Symlinked from ../../packages/my-package`. If symlinking
is _not_ possible the package will be copied. In that case, the console will
diff --git a/doc/06-config.md b/doc/06-config.md
index 59390ea42..f6c2da532 100644
--- a/doc/06-config.md
+++ b/doc/06-config.md
@@ -55,9 +55,15 @@ php_openssl extension in php.ini.
## cafile
-A way to set the path to the openssl CA file. In PHP 5.6+ you should rather
-set this via openssl.cafile in php.ini, although PHP 5.6+ should be able to
-detect your system CA file automatically.
+Location of Certificate Authority file on local filesystem. In PHP 5.6+ you
+should rather set this via openssl.cafile in php.ini, although PHP 5.6+ should
+be able to detect your system CA file automatically.
+
+## capath
+
+If cafile is not specified or if the certificate is not found there, the
+directory pointed to by capath is searched for a suitable certificate.
+capath must be a correctly hashed certificate directory.
## http-basic
diff --git a/doc/articles/custom-installers.md b/doc/articles/custom-installers.md
index 8b3536826..a3c937a5e 100644
--- a/doc/articles/custom-installers.md
+++ b/doc/articles/custom-installers.md
@@ -84,7 +84,7 @@ Example:
"class": "phpDocumentor\\Composer\\TemplateInstallerPlugin"
},
"require": {
- "composer-plugin-api": "1.0.0"
+ "composer-plugin-api": "^1.0"
}
}
```
diff --git a/doc/articles/plugins.md b/doc/articles/plugins.md
index edaa013f0..b4997d22d 100644
--- a/doc/articles/plugins.md
+++ b/doc/articles/plugins.md
@@ -36,7 +36,7 @@ as a normal package's.
The current composer plugin API version is 1.0.0.
-An example of a valid plugin `composer.json` file (with the autoloading
+An example of a valid plugin `composer.json` file (with the autoloading
part omitted):
```json
@@ -89,9 +89,54 @@ Furthermore plugins may implement the
event handlers automatically registered with the `EventDispatcher` when the
plugin is loaded.
-Plugin can subscribe to any of the available [script events](scripts.md#event-names).
+To register a method to an event, implement the method `getSubscribedEvents()`
+and have it return an array. The array key must be the
+[event name](https://getcomposer.org/doc/articles/scripts.md#event-names)
+and the value is the name of the method in this class to be called.
-Example:
+```php
+public static function getSubscribedEvents()
+{
+ return array(
+ 'post-autoload-dump' => 'methodToBeCalled',
+ // ^ event name ^ ^ method name ^
+ );
+}
+```
+
+By default, the priority of an event handler is set to 0. The priorty can be
+changed by attaching a tuple where the first value is the method name, as
+before, and the second value is an integer representing the priority.
+Higher integers represent higher priorities. Priortity 2 is called before
+priority 1, etc.
+
+```php
+public static function getSubscribedEvents()
+{
+ return array(
+ // Will be called before events with priority 0
+ 'post-autoload-dump' => array('methodToBeCalled', 1)
+ );
+}
+```
+
+If multiple methods should be called, then an array of tuples can be attached
+to each event. The tuples do not need to include the priority. If it is
+omitted, it will default to 0.
+
+```php
+public static function getSubscribedEvents()
+{
+ return array(
+ 'post-autoload-dump' => array(
+ array('methodToBeCalled' ), // Priority defaults to 0
+ array('someOtherMethodName', 1), // This fires first
+ )
+ );
+}
+```
+
+Here's a complete example:
```php
**Note:** While this is convenient at times, it should not be how you use
diff --git a/res/composer-schema.json b/res/composer-schema.json
index 966b19b86..9d36b6721 100644
--- a/res/composer-schema.json
+++ b/res/composer-schema.json
@@ -149,6 +149,10 @@
"type": "string",
"description": "A way to set the path to the openssl CA file. In PHP 5.6+ you should rather set this via openssl.cafile in php.ini, although PHP 5.6+ should be able to detect your system CA file automatically."
},
+ "capath": {
+ "type": "string",
+ "description": "If cafile is not specified or if the certificate is not found there, the directory pointed to by capath is searched for a suitable certificate. capath must be a correctly hashed certificate directory."
+ },
"http-basic": {
"type": "object",
"description": "A hash of domain name => {\"username\": \"...\", \"password\": \"...\"}.",
diff --git a/src/Composer/Autoload/ClassMapGenerator.php b/src/Composer/Autoload/ClassMapGenerator.php
index b487ed6ed..3f1243ade 100644
--- a/src/Composer/Autoload/ClassMapGenerator.php
+++ b/src/Composer/Autoload/ClassMapGenerator.php
@@ -18,6 +18,7 @@
namespace Composer\Autoload;
+use Composer\Util\Silencer;
use Symfony\Component\Finder\Finder;
use Composer\IO\IOInterface;
@@ -122,7 +123,7 @@ class ClassMapGenerator
}
try {
- $contents = @php_strip_whitespace($path);
+ $contents = Silencer::call('php_strip_whitespace', $path);
if (!$contents) {
if (!file_exists($path)) {
throw new \Exception('File does not exist');
diff --git a/src/Composer/Cache.php b/src/Composer/Cache.php
index 3ba11da1c..7090a8a0d 100644
--- a/src/Composer/Cache.php
+++ b/src/Composer/Cache.php
@@ -14,6 +14,7 @@ namespace Composer;
use Composer\IO\IOInterface;
use Composer\Util\Filesystem;
+use Composer\Util\Silencer;
use Symfony\Component\Finder\Finder;
/**
@@ -44,7 +45,7 @@ class Cache
$this->filesystem = $filesystem ?: new Filesystem();
if (
- (!is_dir($this->root) && !@mkdir($this->root, 0777, true))
+ (!is_dir($this->root) && !Silencer::call('mkdir', $this->root, 0777, true))
|| !is_writable($this->root)
) {
$this->io->writeError('Cannot create cache directory ' . $this->root . ', or directory is not writable. Proceeding without cache');
@@ -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('Failed to write into cache: '.$e->getMessage().'');
- }
+ $this->io->writeError('Failed to write into cache: '.$e->getMessage().'', 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);
}
diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php
index 8378bc5b4..722bc94cc 100644
--- a/src/Composer/Command/ConfigCommand.php
+++ b/src/Composer/Command/ConfigCommand.php
@@ -12,6 +12,8 @@
namespace Composer\Command;
+use Composer\Util\Platform;
+use Composer\Util\Silencer;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
@@ -142,7 +144,7 @@ EOT
? ($this->config->get('home') . '/config.json')
: ($input->getOption('file') ?: trim(getenv('COMPOSER')) ?: 'composer.json');
- // create global composer.json if this was invoked using `composer global config`
+ // Create global composer.json if this was invoked using `composer global config`
if ($configFile === 'composer.json' && !file_exists($configFile) && realpath(getcwd()) === realpath($this->config->get('home'))) {
file_put_contents($configFile, "{\n}\n");
}
@@ -157,16 +159,16 @@ EOT
$this->authConfigFile = new JsonFile($authConfigFile, null, $io);
$this->authConfigSource = new JsonConfigSource($this->authConfigFile, true);
- // initialize the global file if it's not there
+ // Initialize the global file if it's not there, ignoring any warnings or notices
if ($input->getOption('global') && !$this->configFile->exists()) {
touch($this->configFile->getPath());
$this->configFile->write(array('config' => new \ArrayObject));
- @chmod($this->configFile->getPath(), 0600);
+ Silencer::call('chmod', $this->configFile->getPath(), 0600);
}
if ($input->getOption('global') && !$this->authConfigFile->exists()) {
touch($this->authConfigFile->getPath());
$this->authConfigFile->write(array('http-basic' => new \ArrayObject, 'github-oauth' => new \ArrayObject, 'gitlab-oauth' => new \ArrayObject));
- @chmod($this->authConfigFile->getPath(), 0600);
+ Silencer::call('chmod', $this->authConfigFile->getPath(), 0600);
}
if (!$this->configFile->exists()) {
@@ -183,7 +185,7 @@ EOT
if ($input->getOption('editor')) {
$editor = escapeshellcmd(getenv('EDITOR'));
if (!$editor) {
- if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+ if (Platform::isWindows()) {
$editor = 'notepad';
} else {
foreach (array('vim', 'vi', 'nano', 'pico', 'ed') as $candidate) {
@@ -196,7 +198,7 @@ EOT
}
$file = $input->getOption('auth') ? $this->authConfigFile->getPath() : $this->configFile->getPath();
- system($editor . ' ' . $file . (defined('PHP_WINDOWS_VERSION_BUILD') ? '' : ' > `tty`'));
+ system($editor . ' ' . $file . (Platform::isWindows() ? '' : ' > `tty`'));
return 0;
}
@@ -331,7 +333,11 @@ EOT
'disable-tls' => array($booleanValidator, $booleanNormalizer),
'cafile' => array(
function ($val) { return file_exists($val) && is_readable($val); },
- function ($val) { return $val === 'null' ? null : $val; }
+ function ($val) { return $val === 'null' ? null : $val; },
+ ),
+ 'capath' => array(
+ function ($val) { return is_dir($val) && is_readable($val); },
+ function ($val) { return $val === 'null' ? null : $val; },
),
'github-expose-hostname' => array($booleanValidator, $booleanNormalizer),
);
@@ -434,9 +440,18 @@ EOT
}
if (1 === count($values)) {
- $bool = strtolower($values[0]);
- if (true === $booleanValidator($bool) && false === $booleanNormalizer($bool)) {
- return $this->configSource->addRepository($matches[1], false);
+ $value = strtolower($values[0]);
+ if (true === $booleanValidator($value)) {
+ if (false === $booleanNormalizer($value)) {
+ return $this->configSource->addRepository($matches[1], false);
+ }
+ } else {
+ $value = json_decode($values[0], true);
+ if (JSON_ERROR_NONE !== json_last_error()) {
+ throw new \InvalidArgumentException(sprintf('%s is not valid JSON.', $values[0]));
+ }
+
+ return $this->configSource->addRepository($matches[1], $value);
}
}
diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php
index 144d850a6..ace946f3a 100644
--- a/src/Composer/Command/CreateProjectCommand.php
+++ b/src/Composer/Command/CreateProjectCommand.php
@@ -27,6 +27,7 @@ use Composer\Repository\CompositeRepository;
use Composer\Repository\FilesystemRepository;
use Composer\Repository\InstalledFilesystemRepository;
use Composer\Script\ScriptEvents;
+use Composer\Util\Silencer;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -35,7 +36,6 @@ use Symfony\Component\Finder\Finder;
use Composer\Json\JsonFile;
use Composer\Config\JsonConfigSource;
use Composer\Util\Filesystem;
-use Composer\Util\RemoteFilesystem;
use Composer\Package\Version\VersionParser;
/**
@@ -224,10 +224,10 @@ EOT
chdir($oldCwd);
$vendorComposerDir = $composer->getConfig()->get('vendor-dir').'/composer';
if (is_dir($vendorComposerDir) && $fs->isDirEmpty($vendorComposerDir)) {
- @rmdir($vendorComposerDir);
+ Silencer::call('rmdir', $vendorComposerDir);
$vendorDir = $composer->getConfig()->get('vendor-dir');
if (is_dir($vendorDir) && $fs->isDirEmpty($vendorDir)) {
- @rmdir($vendorDir);
+ Silencer::call('rmdir', $vendorDir);
}
}
@@ -294,7 +294,7 @@ EOT
// handler Ctrl+C for unix-like systems
if (function_exists('pcntl_signal')) {
- declare (ticks = 100);
+ declare(ticks=100);
pcntl_signal(SIGINT, function () use ($directory) {
$fs = new Filesystem();
$fs->removeDirectory($directory);
diff --git a/src/Composer/Command/DependsCommand.php b/src/Composer/Command/DependsCommand.php
index a58e7c5be..abdce5a86 100644
--- a/src/Composer/Command/DependsCommand.php
+++ b/src/Composer/Command/DependsCommand.php
@@ -132,7 +132,7 @@ EOT
} else {
$matchText = '';
if ($input->getOption('match-constraint') !== '*') {
- $matchText = ' in versions '.($matchInvert ? 'not ':'').'matching ' . $input->getOption('match-constraint');
+ $matchText = ' in versions '.($matchInvert ? 'not ' : '').'matching ' . $input->getOption('match-constraint');
}
$io->writeError('There is no installed package depending on "'.$needle.'"'.$matchText.'.');
}
diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php
index faf537a10..2306b138a 100644
--- a/src/Composer/Command/DiagnoseCommand.php
+++ b/src/Composer/Command/DiagnoseCommand.php
@@ -22,6 +22,7 @@ use Composer\Util\ConfigValidator;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\Util\StreamContextFactory;
+use Composer\Util\Keys;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -133,6 +134,9 @@ EOT
$io->write('Checking disk free space: ', false);
$this->outputResult($this->checkDiskSpace($config));
+ $io->write('Checking pubkeys: ', false);
+ $this->outputResult($this->checkPubKeys($config));
+
$io->write('Checking composer version: ', false);
$this->outputResult($this->checkVersion());
@@ -327,6 +331,35 @@ EOT
return true;
}
+ private function checkPubKeys($config)
+ {
+ $home = $config->get('home');
+ $errors = array();
+ $io = $this->getIO();
+
+ if (file_exists($home.'/keys.tags.pub') && file_exists($home.'/keys.dev.pub')) {
+ $io->write('');
+ }
+
+ if (file_exists($home.'/keys.tags.pub')) {
+ $io->write('Tags Public Key Fingerprint: ' . Keys::fingerprint($home.'/keys.tags.pub'));
+ } else {
+ $errors[] = 'Missing pubkey for tags verification';
+ }
+
+ if (file_exists($home.'/keys.dev.pub')) {
+ $io->write('Dev Public Key Fingerprint: ' . Keys::fingerprint($home.'/keys.dev.pub'));
+ } else {
+ $errors[] = 'Missing pubkey for dev verification';
+ }
+
+ if ($errors) {
+ $errors[] = 'Run composer self-update --update-keys to set them up';
+ }
+
+ return $errors ?: true;
+ }
+
private function checkVersion()
{
$protocol = extension_loaded('openssl') ? 'https' : 'http';
diff --git a/src/Composer/Command/HomeCommand.php b/src/Composer/Command/HomeCommand.php
index fff1f86eb..150b8c71b 100644
--- a/src/Composer/Command/HomeCommand.php
+++ b/src/Composer/Command/HomeCommand.php
@@ -16,6 +16,7 @@ use Composer\Factory;
use Composer\Package\CompletePackageInterface;
use Composer\Repository\RepositoryInterface;
use Composer\Repository\ArrayRepository;
+use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
@@ -117,7 +118,7 @@ EOT
{
$url = ProcessExecutor::escape($url);
- if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+ if (Platform::isWindows()) {
return passthru('start "web" explorer "' . $url . '"');
}
diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php
index 869720718..e9bd610b4 100644
--- a/src/Composer/Command/RemoveCommand.php
+++ b/src/Composer/Command/RemoveCommand.php
@@ -37,6 +37,7 @@ class RemoveCommand extends Command
->setDefinition(array(
new InputArgument('packages', InputArgument::IS_ARRAY, 'Packages that should be removed.'),
new InputOption('dev', null, InputOption::VALUE_NONE, 'Removes a package from the require-dev section.'),
+ new InputOption('no-plugins', null, InputOption::VALUE_NONE, 'Disables all plugins.'),
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'),
new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'),
@@ -92,7 +93,7 @@ EOT
}
// Update packages
- $composer = $this->getComposer();
+ $composer = $this->getComposer(true, $input->getOption('no-plugins'));
$composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output);
diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php
index bbdf15681..cd62bbc5a 100644
--- a/src/Composer/Command/RequireCommand.php
+++ b/src/Composer/Command/RequireCommand.php
@@ -42,6 +42,7 @@ class RequireCommand extends InitCommand
new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'),
new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'),
new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist even for dev versions.'),
+ new InputOption('no-plugins', null, InputOption::VALUE_NONE, 'Disables all plugins.'),
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'),
new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'),
@@ -93,7 +94,7 @@ EOT
$composerDefinition = $json->read();
$composerBackup = file_get_contents($json->getPath());
- $composer = $this->getComposer();
+ $composer = $this->getComposer(true, $input->getOption('no-plugins'));
$repos = $composer->getRepositoryManager()->getRepositories();
$platformOverrides = $composer->getConfig()->get('platform') ?: array();
@@ -143,7 +144,7 @@ EOT
// Update packages
$this->resetComposer();
- $composer = $this->getComposer();
+ $composer = $this->getComposer(true, $input->getOption('no-plugins'));
$composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress'));
$commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output);
diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php
index 581c45ab6..16b98020a 100644
--- a/src/Composer/Command/SelfUpdateCommand.php
+++ b/src/Composer/Command/SelfUpdateCommand.php
@@ -14,8 +14,10 @@ namespace Composer\Command;
use Composer\Composer;
use Composer\Factory;
+use Composer\Config;
use Composer\Util\Filesystem;
-use Composer\Util\RemoteFilesystem;
+use Composer\Util\Keys;
+use Composer\IO\IOInterface;
use Composer\Downloader\FilesystemException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -44,6 +46,7 @@ class SelfUpdateCommand extends Command
new InputOption('clean-backups', null, InputOption::VALUE_NONE, 'Delete old backups during an update. This makes the current version of composer the only backup available after the update'),
new InputArgument('version', InputArgument::OPTIONAL, 'The version to update to'),
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
+ new InputOption('update-keys', null, InputOption::VALUE_NONE, 'Prompt user for a key update'),
))
->setHelp(<<self-update 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 %s.", $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 %s.", $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('The download of the new composer version failed for an unexpected reason');
return 1;
}
+ // verify phar signature
+ if (!extension_loaded('openssl') && $config->get('disable-tls')) {
+ $io->writeError('Skipping phar signature verification as you have disabled OpenSSL via config.disable-tls');
+ } 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', <<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 https://composer.github.io/pubkeys.html 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)) {
diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php
index 7704e09fb..5ddce2954 100644
--- a/src/Composer/Command/ShowCommand.php
+++ b/src/Composer/Command/ShowCommand.php
@@ -20,6 +20,7 @@ use Composer\Semver\VersionParser;
use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents;
use Composer\Package\PackageInterface;
+use Composer\Util\Platform;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
@@ -232,7 +233,7 @@ EOT
// outside of a real terminal, use space without a limit
$width = PHP_INT_MAX;
}
- if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+ if (Platform::isWindows()) {
$width--;
}
@@ -246,10 +247,10 @@ EOT
$writeDescription = !$input->getOption('name-only') && !$input->getOption('path') && ($nameLength + ($showVersion ? $versionLength : 0) + 24 <= $width);
foreach ($packages[$type] as $package) {
if (is_object($package)) {
- $output->write($indent . str_pad($package->getPrettyName(), $nameLength, ' '), false);
+ $io->write($indent . str_pad($package->getPrettyName(), $nameLength, ' '), false);
if ($writeVersion) {
- $output->write(' ' . str_pad($package->getFullPrettyVersion(), $versionLength, ' '), false);
+ $io->write(' ' . str_pad($package->getFullPrettyVersion(), $versionLength, ' '), false);
}
if ($writeDescription) {
@@ -258,15 +259,15 @@ EOT
if (strlen($description) > $remaining) {
$description = substr($description, 0, $remaining - 3) . '...';
}
- $output->write(' ' . $description);
+ $io->write(' ' . $description, false);
}
if ($writePath) {
$path = strtok(realpath($composer->getInstallationManager()->getInstallPath($package)), "\r\n");
- $output->write(' ' . $path);
+ $io->write(' ' . $path, false);
}
} else {
- $output->write($indent . $package);
+ $io->write($indent . $package, false);
}
$io->write('');
}
@@ -458,7 +459,7 @@ EOT
/**
* Init styles for tree
*
- * @param OutputInterface $output
+ * @param OutputInterface $output
*/
protected function initStyles(OutputInterface $output)
{
@@ -479,20 +480,20 @@ EOT
/**
* Display the tree
*
- * @param PackageInterface|string $package
- * @param RepositoryInterface $installedRepo
- * @param RepositoryInterface $distantRepos
- * @param OutputInterface $output
+ * @param PackageInterface|string $package
+ * @param RepositoryInterface $installedRepo
+ * @param RepositoryInterface $distantRepos
+ * @param OutputInterface $output
*/
protected function displayPackageTree(PackageInterface $package, RepositoryInterface $installedRepo, RepositoryInterface $distantRepos, OutputInterface $output)
{
$packagesInTree = array();
$packagesInTree[] = $package;
- $output->write(sprintf('%s', $package->getPrettyName()));
- $output->write(' ' . $package->getPrettyVersion());
- $output->write(' ' . strtok($package->getDescription(), "\r\n"));
- $output->writeln('');
+ $io = $this->getIO();
+ $io->write(sprintf('%s', $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)
{
diff --git a/src/Composer/Config.php b/src/Composer/Config.php
index c7d8efa8e..2b6d14da7 100644
--- a/src/Composer/Config.php
+++ b/src/Composer/Config.php
@@ -47,6 +47,7 @@ class Config
'github-domains' => array('github.com'),
'disable-tls' => false,
'cafile' => null,
+ 'capath' => null,
'github-expose-hostname' => true,
'gitlab-domains' => array('gitlab.com'),
'store-auths' => 'prompt',
@@ -179,6 +180,7 @@ class Config
case 'cache-repo-dir':
case 'cache-vcs-dir':
case 'cafile':
+ case 'capath':
// convert foo-bar to COMPOSER_FOO_BAR and check if it exists since it overrides the local config
$env = 'COMPOSER_' . strtoupper(strtr($key, '-', '_'));
@@ -189,7 +191,7 @@ class Config
return $val;
}
- return ($flags & self::RELATIVE_PATHS == self::RELATIVE_PATHS) ? $val : $this->realpath($val);
+ return (($flags & self::RELATIVE_PATHS) == self::RELATIVE_PATHS) ? $val : $this->realpath($val);
case 'cache-ttl':
return (int) $this->config[$key];
@@ -343,7 +345,7 @@ class Config
*/
private function realpath($path)
{
- if (substr($path, 0, 1) === '/' || substr($path, 1, 1) === ':') {
+ if (preg_match('{^(?:/|[a-z]:|[a-z0-9.]+://)}i', $path)) {
return $path;
}
diff --git a/src/Composer/Config/JsonConfigSource.php b/src/Composer/Config/JsonConfigSource.php
index 5df29f032..2b6d13096 100644
--- a/src/Composer/Config/JsonConfigSource.php
+++ b/src/Composer/Config/JsonConfigSource.php
@@ -14,6 +14,7 @@ namespace Composer\Config;
use Composer\Json\JsonFile;
use Composer\Json\JsonManipulator;
+use Composer\Util\Silencer;
/**
* JSON Configuration Source
@@ -173,7 +174,7 @@ class JsonConfigSource implements ConfigSourceInterface
}
if ($newFile) {
- @chmod($this->file->getPath(), 0600);
+ Silencer::call('chmod', $this->file->getPath(), 0600);
}
}
diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php
index 756cf18c6..07d517eae 100644
--- a/src/Composer/Console/Application.php
+++ b/src/Composer/Console/Application.php
@@ -12,10 +12,11 @@
namespace Composer\Console;
+use Composer\Util\Platform;
+use Composer\Util\Silencer;
use Symfony\Component\Console\Application as BaseApplication;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
-use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Formatter\OutputFormatter;
@@ -64,7 +65,7 @@ class Application extends BaseApplication
}
if (function_exists('date_default_timezone_set') && function_exists('date_default_timezone_get')) {
- date_default_timezone_set(@date_default_timezone_get());
+ date_default_timezone_set(Silencer::call('date_default_timezone_get'));
}
if (!$shutdownRegistered) {
@@ -136,9 +137,7 @@ class Application extends BaseApplication
if ($newWorkDir = $this->getNewWorkingDir($input)) {
$oldWorkingDir = getcwd();
chdir($newWorkDir);
- if ($io->isDebug() >= 4) {
- $io->writeError('Changed CWD to ' . getcwd());
- }
+ $io->writeError('Changed CWD to ' . getcwd(), true, IOInterface::DEBUG);
}
// add non-standard scripts as own commands
@@ -203,30 +202,32 @@ class Application extends BaseApplication
{
$io = $this->getIO();
+ Silencer::suppress();
try {
$composer = $this->getComposer(false, true);
if ($composer) {
$config = $composer->getConfig();
$minSpaceFree = 1024 * 1024;
- if ((($df = @disk_free_space($dir = $config->get('home'))) !== false && $df < $minSpaceFree)
- || (($df = @disk_free_space($dir = $config->get('vendor-dir'))) !== false && $df < $minSpaceFree)
- || (($df = @disk_free_space($dir = sys_get_temp_dir())) !== false && $df < $minSpaceFree)
+ if ((($df = disk_free_space($dir = $config->get('home'))) !== false && $df < $minSpaceFree)
+ || (($df = disk_free_space($dir = $config->get('vendor-dir'))) !== false && $df < $minSpaceFree)
+ || (($df = disk_free_space($dir = sys_get_temp_dir())) !== false && $df < $minSpaceFree)
) {
- $io->writeError('The disk hosting '.$dir.' is full, this may be the cause of the following exception');
+ $io->writeError('The disk hosting '.$dir.' is full, this may be the cause of the following exception', 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('The following exception may be caused by a stale entry in your cmd.exe AutoRun');
- $io->writeError('Check https://getcomposer.org/doc/articles/troubleshooting.md#-the-system-cannot-find-the-path-specified-windows- for details');
+ if (Platform::isWindows() && false !== strpos($exception->getMessage(), 'The system cannot find the path specified')) {
+ $io->writeError('The following exception may be caused by a stale entry in your cmd.exe AutoRun', true, IOInterface::QUIET);
+ $io->writeError('Check https://getcomposer.org/doc/articles/troubleshooting.md#-the-system-cannot-find-the-path-specified-windows- for details', true, IOInterface::QUIET);
}
if (false !== strpos($exception->getMessage(), 'fork failed - Cannot allocate memory')) {
- $io->writeError('The following exception is caused by a lack of memory and not having swap configured');
- $io->writeError('Check https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors for details');
+ $io->writeError('The following exception is caused by a lack of memory and not having swap configured', true, IOInterface::QUIET);
+ $io->writeError('Check https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors for details', true, IOInterface::QUIET);
}
}
diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php
index 9973f9d39..c6092c28c 100644
--- a/src/Composer/DependencyResolver/SolverProblemsException.php
+++ b/src/Composer/DependencyResolver/SolverProblemsException.php
@@ -31,12 +31,21 @@ class SolverProblemsException extends \RuntimeException
protected function createMessage()
{
$text = "\n";
+ $hasExtensionProblems = false;
foreach ($this->problems as $i => $problem) {
$text .= " Problem ".($i + 1).$problem->getPrettyString($this->installedMap)."\n";
+
+ if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) {
+ $hasExtensionProblems = true;
+ }
}
if (strpos($text, 'could not be found') || strpos($text, 'no matching package found')) {
- $text .= "\nPotential causes:\n - A typo in the package name\n - The package is not available in a stable-enough version according to your minimum-stability setting\n see for more details.\n\nRead 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 for more details.\n\nRead 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;
+ }
}
diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php
index f42b82872..9fa6a3338 100644
--- a/src/Composer/Downloader/ArchiveDownloader.php
+++ b/src/Composer/Downloader/ArchiveDownloader.php
@@ -14,6 +14,7 @@ namespace Composer\Downloader;
use Composer\Package\PackageInterface;
use Symfony\Component\Finder\Finder;
+use Composer\IO\IOInterface;
/**
* Base downloader for archives
@@ -34,9 +35,7 @@ abstract class ArchiveDownloader extends FileDownloader
while ($retries--) {
$fileName = parent::download($package, $path);
- if ($this->io->isVerbose()) {
- $this->io->writeError(' Extracting archive');
- }
+ $this->io->writeError(' Extracting archive', true, IOInterface::VERBOSE);
try {
$this->filesystem->ensureDirectoryExists($temporaryDir);
diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php
index d3a57500c..ece495dba 100644
--- a/src/Composer/Downloader/FileDownloader.php
+++ b/src/Composer/Downloader/FileDownloader.php
@@ -141,9 +141,7 @@ class FileDownloader implements DownloaderInterface
if ((0 !== $e->getCode() && !in_array($e->getCode(), array(500, 502, 503, 504))) || !$retries) {
throw $e;
}
- if ($this->io->isVerbose()) {
- $this->io->writeError(' Download failed, retrying...');
- }
+ $this->io->writeError(' Download failed, retrying...', true, IOInterface::VERBOSE);
usleep(500000);
}
}
diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php
index 7db561889..48e0d4b39 100644
--- a/src/Composer/Downloader/GitDownloader.php
+++ b/src/Composer/Downloader/GitDownloader.php
@@ -14,6 +14,7 @@ namespace Composer\Downloader;
use Composer\Package\PackageInterface;
use Composer\Util\Git as GitUtil;
+use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Composer\IO\IOInterface;
use Composer\Util\Filesystem;
@@ -43,7 +44,7 @@ class GitDownloader extends VcsDownloader
$path = $this->normalizePath($path);
$ref = $package->getSourceReference();
- $flag = defined('PHP_WINDOWS_VERSION_MAJOR') ? '/D ' : '';
+ $flag = Platform::isWindows() ? '/D ' : '';
$command = 'git clone --no-checkout %s %s && cd '.$flag.'%2$s && git remote add composer %1$s && git fetch composer';
$this->io->writeError(" Cloning ".$ref);
@@ -353,7 +354,7 @@ class GitDownloader extends VcsDownloader
protected function normalizePath($path)
{
- if (defined('PHP_WINDOWS_VERSION_MAJOR') && strlen($path) > 0) {
+ if (Platform::isWindows() && strlen($path) > 0) {
$basePath = $path;
$removed = array();
diff --git a/src/Composer/Downloader/GzipDownloader.php b/src/Composer/Downloader/GzipDownloader.php
index ae7d7f17a..9969578e5 100644
--- a/src/Composer/Downloader/GzipDownloader.php
+++ b/src/Composer/Downloader/GzipDownloader.php
@@ -16,6 +16,7 @@ use Composer\Config;
use Composer\Cache;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface;
+use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\IO\IOInterface;
@@ -40,25 +41,26 @@ class GzipDownloader extends ArchiveDownloader
$targetFilepath = $path . DIRECTORY_SEPARATOR . basename(substr($file, 0, -3));
// Try to use gunzip on *nix
- if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
+ if (!Platform::isWindows()) {
$command = 'gzip -cd ' . ProcessExecutor::escape($file) . ' > ' . ProcessExecutor::escape($targetFilepath);
if (0 === $this->process->execute($command, $ignoredOutput)) {
return;
}
+ if (extension_loaded('zlib')) {
+ // Fallback to using the PHP extension.
+ $this->extractUsingExt($file, $targetFilepath);
+
+ return;
+ }
+
$processError = 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput();
throw new \RuntimeException($processError);
}
// Windows version of PHP has built-in support of gzip functions
- $archiveFile = gzopen($file, 'rb');
- $targetFile = fopen($targetFilepath, 'wb');
- while ($string = gzread($archiveFile, 4096)) {
- fwrite($targetFile, $string, strlen($string));
- }
- gzclose($archiveFile);
- fclose($targetFile);
+ $this->extractUsingExt($file, $targetFilepath);
}
/**
@@ -68,4 +70,15 @@ class GzipDownloader extends ArchiveDownloader
{
return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
}
+
+ private function extractUsingExt($file, $targetFilepath)
+ {
+ $archiveFile = gzopen($file, 'rb');
+ $targetFile = fopen($targetFilepath, 'wb');
+ while ($string = gzread($archiveFile, 4096)) {
+ fwrite($targetFile, $string, strlen($string));
+ }
+ gzclose($archiveFile);
+ fclose($targetFile);
+ }
}
diff --git a/src/Composer/Downloader/RarDownloader.php b/src/Composer/Downloader/RarDownloader.php
index 81e11785e..2a0c98cf9 100644
--- a/src/Composer/Downloader/RarDownloader.php
+++ b/src/Composer/Downloader/RarDownloader.php
@@ -15,6 +15,7 @@ namespace Composer\Downloader;
use Composer\Config;
use Composer\Cache;
use Composer\EventDispatcher\EventDispatcher;
+use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\IO\IOInterface;
@@ -42,7 +43,7 @@ class RarDownloader extends ArchiveDownloader
$processError = null;
// Try to use unrar on *nix
- if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
+ if (!Platform::isWindows()) {
$command = 'unrar x ' . ProcessExecutor::escape($file) . ' ' . ProcessExecutor::escape($path) . ' && chmod -R u+w ' . ProcessExecutor::escape($path);
if (0 === $this->process->execute($command, $ignoredOutput)) {
@@ -65,7 +66,7 @@ class RarDownloader extends ArchiveDownloader
$error = "Could not decompress the archive, enable the PHP rar extension or install unrar.\n"
. $iniMessage . "\n" . $processError;
- if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
+ if (!Platform::isWindows()) {
$error = "Could not decompress the archive, enable the PHP rar extension.\n" . $iniMessage;
}
diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php
index 6faaaaa4f..5f483975c 100644
--- a/src/Composer/Downloader/ZipDownloader.php
+++ b/src/Composer/Downloader/ZipDownloader.php
@@ -15,6 +15,7 @@ namespace Composer\Downloader;
use Composer\Config;
use Composer\Cache;
use Composer\EventDispatcher\EventDispatcher;
+use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\IO\IOInterface;
@@ -38,7 +39,7 @@ class ZipDownloader extends ArchiveDownloader
$processError = null;
// try to use unzip on *nix
- if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
+ if (!Platform::isWindows()) {
$command = 'unzip '.ProcessExecutor::escape($file).' -d '.ProcessExecutor::escape($path) . ' && chmod -R u+w ' . ProcessExecutor::escape($path);
try {
if (0 === $this->process->execute($command, $ignoredOutput)) {
@@ -64,7 +65,7 @@ class ZipDownloader extends ArchiveDownloader
$error = "Could not decompress the archive, enable the PHP zip extension or install unzip.\n"
. $iniMessage . "\n" . $processError;
- if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
+ if (!Platform::isWindows()) {
$error = "Could not decompress the archive, enable the PHP zip extension.\n" . $iniMessage;
}
diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php
index c669b9dab..b9a879a01 100644
--- a/src/Composer/EventDispatcher/EventDispatcher.php
+++ b/src/Composer/EventDispatcher/EventDispatcher.php
@@ -155,9 +155,7 @@ class EventDispatcher
$event = $this->checkListenerExpectedEvent($callable, $event);
$return = false === call_user_func($callable, $event) ? 1 : 0;
} elseif ($this->isComposerScript($callable)) {
- if ($this->io->isVerbose()) {
- $this->io->writeError(sprintf('> %s: %s', $event->getName(), $callable));
- }
+ $this->io->writeError(sprintf('> %s: %s', $event->getName(), $callable), true, IOInterface::VERBOSE);
$scriptName = substr($callable, 1);
$args = $event->getArguments();
$flags = $event->getFlags();
diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php
index 60431229b..2f3491fd4 100644
--- a/src/Composer/Factory.php
+++ b/src/Composer/Factory.php
@@ -20,8 +20,10 @@ use Composer\Package\Version\VersionGuesser;
use Composer\Repository\RepositoryManager;
use Composer\Repository\WritableRepositoryInterface;
use Composer\Util\Filesystem;
+use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
+use Composer\Util\Silencer;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Autoload\AutoloadGenerator;
@@ -40,8 +42,8 @@ use Seld\JsonLint\JsonParser;
class Factory
{
/**
- * @return string
* @throws \RuntimeException
+ * @return string
*/
protected static function getHomeDir()
{
@@ -50,7 +52,7 @@ class Factory
return $home;
}
- if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+ if (Platform::isWindows()) {
if (!getenv('APPDATA')) {
throw new \RuntimeException('The APPDATA or COMPOSER_HOME environment variable must be set for composer to run correctly');
}
@@ -89,7 +91,7 @@ class Factory
return $homeEnv . '/cache';
}
- if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+ if (Platform::isWindows()) {
if ($cacheDir = getenv('LOCALAPPDATA')) {
$cacheDir .= '/Composer';
} else {
@@ -114,7 +116,7 @@ class Factory
}
/**
- * @param string $home
+ * @param string $home
* @return string
*/
protected static function getDataDir($home)
@@ -124,7 +126,7 @@ class Factory
return $homeEnv;
}
- if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+ if (Platform::isWindows()) {
return strtr($home, '\\', '/');
}
@@ -139,7 +141,7 @@ class Factory
}
/**
- * @param IOInterface|null $io
+ * @param IOInterface|null $io
* @return Config
*/
public static function createConfig(IOInterface $io = null, $cwd = null)
@@ -163,9 +165,9 @@ class Factory
foreach ($dirs as $dir) {
if (!file_exists($dir . '/.htaccess')) {
if (!is_dir($dir)) {
- @mkdir($dir, 0777, true);
+ Silencer::call('mkdir', $dir, 0777, true);
}
- @file_put_contents($dir . '/.htaccess', 'Deny from all');
+ Silencer::call('file_put_contents', $dir . '/.htaccess', 'Deny from all');
}
}
@@ -189,6 +191,20 @@ class Factory
}
$config->setAuthConfigSource(new JsonConfigSource($file, true));
+ // load COMPOSER_AUTH environment variable if set
+ if ($composerAuthEnv = getenv('COMPOSER_AUTH')) {
+ $authData = json_decode($composerAuthEnv, true);
+
+ if (is_null($authData)) {
+ throw new \UnexpectedValueException('COMPOSER_AUTH environment variable is malformed, should be a valid JSON object');
+ }
+
+ if ($io && $io->isDebug()) {
+ $io->writeError('Loading auth config from COMPOSER_AUTH');
+ }
+ $config->merge(array('config' => $authData));
+ }
+
return $config;
}
@@ -292,14 +308,10 @@ class Factory
$config = static::createConfig($io, $cwd);
$config->merge($localConfig);
if (isset($composerFile)) {
- if ($io && $io->isDebug()) {
- $io->writeError('Loading config file ' . $composerFile);
- }
+ $io->writeError('Loading config file ' . $composerFile, true, IOInterface::DEBUG);
$localAuthFile = new JsonFile(dirname(realpath($composerFile)) . '/auth.json');
if ($localAuthFile->exists()) {
- if ($io && $io->isDebug()) {
- $io->writeError('Loading config file ' . $localAuthFile->getPath());
- }
+ $io->writeError('Loading config file ' . $localAuthFile->getPath(), true, IOInterface::DEBUG);
$config->merge(array('config' => $localAuthFile->read()));
$config->setAuthConfigSource(new JsonConfigSource($localAuthFile, true));
}
@@ -434,9 +446,7 @@ class Factory
try {
$composer = self::createComposer($io, $config->get('home') . '/composer.json', $disablePlugins, $config->get('home'), false);
} catch (\Exception $e) {
- if ($io->isDebug()) {
- $io->writeError('Failed to initialize global composer: '.$e->getMessage());
- }
+ $io->writeError('Failed to initialize global composer: '.$e->getMessage(), true, IOInterface::DEBUG);
}
return $composer;
@@ -568,9 +578,9 @@ class Factory
}
/**
- * @param IOInterface $io IO instance
- * @param Config $config Config instance
- * @param array $options Array of options passed directly to RemoteFilesystem constructor
+ * @param IOInterface $io IO instance
+ * @param Config $config Config instance
+ * @param array $options Array of options passed directly to RemoteFilesystem constructor
* @return RemoteFilesystem
*/
public static function createRemoteFilesystem(IOInterface $io, Config $config = null, $options = array())
@@ -590,9 +600,12 @@ class Factory
$remoteFilesystemOptions = array();
if ($disableTls === false) {
if ($config && $config->get('cafile')) {
- $remoteFilesystemOptions = array('ssl' => array('cafile' => $config->get('cafile')));
+ $remoteFilesystemOptions['ssl']['cafile'] = $config->get('cafile');
}
- $remoteFilesystemOptions = array_merge_recursive($remoteFilesystemOptions, $options);
+ if ($config && $config->get('capath')) {
+ $remoteFilesystemOptions['ssl']['capath'] = $config->get('capath');
+ }
+ $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()
{
diff --git a/src/Composer/IO/BaseIO.php b/src/Composer/IO/BaseIO.php
index 139e58723..ad7e32df4 100644
--- a/src/Composer/IO/BaseIO.php
+++ b/src/Composer/IO/BaseIO.php
@@ -60,27 +60,25 @@ abstract class BaseIO implements IOInterface
*/
public function loadConfiguration(Config $config)
{
+ $githubOauth = $config->get('github-oauth') ?: array();
+ $gitlabOauth = $config->get('gitlab-oauth') ?: array();
+ $httpBasic = $config->get('http-basic') ?: array();
+
// reload oauth token from config if available
- if ($tokens = $config->get('github-oauth')) {
- foreach ($tokens as $domain => $token) {
- if (!preg_match('{^[a-z0-9]+$}', $token)) {
- throw new \UnexpectedValueException('Your github oauth token for '.$domain.' contains invalid characters: "'.$token.'"');
- }
- $this->setAuthentication($domain, $token, 'x-oauth-basic');
+ foreach ($githubOauth as $domain => $token) {
+ if (!preg_match('{^[a-z0-9]+$}', $token)) {
+ throw new \UnexpectedValueException('Your github oauth token for '.$domain.' contains invalid characters: "'.$token.'"');
}
+ $this->setAuthentication($domain, $token, 'x-oauth-basic');
}
- if ($tokens = $config->get('gitlab-oauth')) {
- foreach ($tokens as $domain => $token) {
- $this->setAuthentication($domain, $token, 'oauth2');
- }
+ foreach ($gitlabOauth as $domain => $token) {
+ $this->setAuthentication($domain, $token, 'oauth2');
}
// reload http basic credentials from config if available
- if ($creds = $config->get('http-basic')) {
- foreach ($creds as $domain => $cred) {
- $this->setAuthentication($domain, $cred['username'], $cred['password']);
- }
+ foreach ($httpBasic as $domain => $cred) {
+ $this->setAuthentication($domain, $cred['username'], $cred['password']);
}
// setup process timeout
diff --git a/src/Composer/IO/BufferIO.php b/src/Composer/IO/BufferIO.php
index db3fb634b..1069c0d9a 100644
--- a/src/Composer/IO/BufferIO.php
+++ b/src/Composer/IO/BufferIO.php
@@ -35,7 +35,7 @@ class BufferIO extends ConsoleIO
$input = new StringInput($input);
$input->setInteractive(false);
- $output = new StreamOutput(fopen('php://memory', 'rw'), $verbosity, !empty($formatter), $formatter);
+ $output = new StreamOutput(fopen('php://memory', 'rw'), $verbosity, $formatter ? $formatter->isDecorated() : false, $formatter);
parent::__construct($input, $output, new HelperSet(array()));
}
diff --git a/src/Composer/IO/ConsoleIO.php b/src/Composer/IO/ConsoleIO.php
index 3867695f1..f97af2e8a 100644
--- a/src/Composer/IO/ConsoleIO.php
+++ b/src/Composer/IO/ConsoleIO.php
@@ -33,6 +33,7 @@ class ConsoleIO extends BaseIO
protected $lastMessage;
protected $lastMessageErr;
private $startTime;
+ private $verbosityMap;
/**
* Constructor.
@@ -46,6 +47,13 @@ class ConsoleIO extends BaseIO
$this->input = $input;
$this->output = $output;
$this->helperSet = $helperSet;
+ $this->verbosityMap = array(
+ self::QUIET => OutputInterface::VERBOSITY_QUIET,
+ self::NORMAL => OutputInterface::VERBOSITY_NORMAL,
+ self::VERBOSE => OutputInterface::VERBOSITY_VERBOSE,
+ self::VERY_VERBOSE => OutputInterface::VERBOSITY_VERY_VERBOSE,
+ self::DEBUG => OutputInterface::VERBOSITY_DEBUG,
+ );
}
public function enableDebugging($startTime)
@@ -96,26 +104,32 @@ class ConsoleIO extends BaseIO
/**
* {@inheritDoc}
*/
- public function write($messages, $newline = true)
+ public function write($messages, $newline = true, $verbosity = self::NORMAL)
{
- $this->doWrite($messages, $newline, false);
+ $this->doWrite($messages, $newline, false, $verbosity);
}
/**
* {@inheritDoc}
*/
- public function writeError($messages, $newline = true)
+ public function writeError($messages, $newline = true, $verbosity = self::NORMAL)
{
- $this->doWrite($messages, $newline, true);
+ $this->doWrite($messages, $newline, true, $verbosity);
}
/**
* @param array|string $messages
* @param bool $newline
* @param bool $stderr
+ * @param int $verbosity
*/
- private function doWrite($messages, $newline, $stderr)
+ private function doWrite($messages, $newline, $stderr, $verbosity)
{
+ $sfVerbosity = $this->verbosityMap[$verbosity];
+ if ($sfVerbosity > $this->output->getVerbosity()) {
+ return;
+ }
+
if (null !== $this->startTime) {
$memoryUsage = memory_get_usage() / 1024 / 1024;
$timeSpent = microtime(true) - $this->startTime;
@@ -125,30 +139,30 @@ class ConsoleIO extends BaseIO
}
if (true === $stderr && $this->output instanceof ConsoleOutputInterface) {
- $this->output->getErrorOutput()->write($messages, $newline);
+ $this->output->getErrorOutput()->write($messages, $newline, $sfVerbosity);
$this->lastMessageErr = join($newline ? "\n" : '', (array) $messages);
return;
}
- $this->output->write($messages, $newline);
+ $this->output->write($messages, $newline, $sfVerbosity);
$this->lastMessage = join($newline ? "\n" : '', (array) $messages);
}
/**
* {@inheritDoc}
*/
- public function overwrite($messages, $newline = true, $size = null)
+ public function overwrite($messages, $newline = true, $size = null, $verbosity = self::NORMAL)
{
- $this->doOverwrite($messages, $newline, $size, false);
+ $this->doOverwrite($messages, $newline, $size, false, $verbosity);
}
/**
* {@inheritDoc}
*/
- public function overwriteError($messages, $newline = true, $size = null)
+ public function overwriteError($messages, $newline = true, $size = null, $verbosity = self::NORMAL)
{
- $this->doOverwrite($messages, $newline, $size, true);
+ $this->doOverwrite($messages, $newline, $size, true, $verbosity);
}
/**
@@ -156,8 +170,9 @@ class ConsoleIO extends BaseIO
* @param bool $newline
* @param int|null $size
* @param bool $stderr
+ * @param int $verbosity
*/
- private function doOverwrite($messages, $newline, $size, $stderr)
+ private function doOverwrite($messages, $newline, $size, $stderr, $verbosity)
{
// messages can be an array, let's convert it to string anyway
$messages = join($newline ? "\n" : '', (array) $messages);
@@ -168,21 +183,21 @@ class ConsoleIO extends BaseIO
$size = strlen(strip_tags($stderr ? $this->lastMessageErr : $this->lastMessage));
}
// ...let's fill its length with backspaces
- $this->doWrite(str_repeat("\x08", $size), false, $stderr);
+ $this->doWrite(str_repeat("\x08", $size), false, $stderr, $verbosity);
// write the new message
- $this->doWrite($messages, false, $stderr);
+ $this->doWrite($messages, false, $stderr, $verbosity);
$fill = $size - strlen(strip_tags($messages));
if ($fill > 0) {
// whitespace whatever has left
- $this->doWrite(str_repeat(' ', $fill), false, $stderr);
+ $this->doWrite(str_repeat(' ', $fill), false, $stderr, $verbosity);
// move the cursor back
- $this->doWrite(str_repeat("\x08", $fill), false, $stderr);
+ $this->doWrite(str_repeat("\x08", $fill), false, $stderr, $verbosity);
}
if ($newline) {
- $this->doWrite('', true, $stderr);
+ $this->doWrite('', true, $stderr, $verbosity);
}
if ($stderr) {
diff --git a/src/Composer/IO/IOInterface.php b/src/Composer/IO/IOInterface.php
index 3165b0d24..ff20a591d 100644
--- a/src/Composer/IO/IOInterface.php
+++ b/src/Composer/IO/IOInterface.php
@@ -21,6 +21,12 @@ use Composer\Config;
*/
interface IOInterface
{
+ const QUIET = 1;
+ const NORMAL = 2;
+ const VERBOSE = 4;
+ const VERY_VERBOSE = 8;
+ const DEBUG = 16;
+
/**
* Is this input means interactive?
*
@@ -59,36 +65,40 @@ interface IOInterface
/**
* Writes a message to the output.
*
- * @param string|array $messages The message as an array of lines or a single string
- * @param bool $newline Whether to add a newline or not
+ * @param string|array $messages The message as an array of lines or a single string
+ * @param bool $newline Whether to add a newline or not
+ * @param int $verbosity Verbosity level from the VERBOSITY_* constants
*/
- public function write($messages, $newline = true);
+ public function write($messages, $newline = true, $verbosity = self::NORMAL);
/**
* Writes a message to the error output.
*
- * @param string|array $messages The message as an array of lines or a single string
- * @param bool $newline Whether to add a newline or not
+ * @param string|array $messages The message as an array of lines or a single string
+ * @param bool $newline Whether to add a newline or not
+ * @param int $verbosity Verbosity level from the VERBOSITY_* constants
*/
- public function writeError($messages, $newline = true);
+ public function writeError($messages, $newline = true, $verbosity = self::NORMAL);
/**
* Overwrites a previous message to the output.
*
- * @param string|array $messages The message as an array of lines or a single string
- * @param bool $newline Whether to add a newline or not
- * @param int $size The size of line
+ * @param string|array $messages The message as an array of lines or a single string
+ * @param bool $newline Whether to add a newline or not
+ * @param int $size The size of line
+ * @param int $verbosity Verbosity level from the VERBOSITY_* constants
*/
- public function overwrite($messages, $newline = true, $size = null);
+ public function overwrite($messages, $newline = true, $size = null, $verbosity = self::NORMAL);
/**
* Overwrites a previous message to the error output.
*
- * @param string|array $messages The message as an array of lines or a single string
- * @param bool $newline Whether to add a newline or not
- * @param int $size The size of line
+ * @param string|array $messages The message as an array of lines or a single string
+ * @param bool $newline Whether to add a newline or not
+ * @param int $size The size of line
+ * @param int $verbosity Verbosity level from the VERBOSITY_* constants
*/
- public function overwriteError($messages, $newline = true, $size = null);
+ public function overwriteError($messages, $newline = true, $size = null, $verbosity = self::NORMAL);
/**
* Asks a question to the user.
diff --git a/src/Composer/IO/NullIO.php b/src/Composer/IO/NullIO.php
index 1a88395d3..587168677 100644
--- a/src/Composer/IO/NullIO.php
+++ b/src/Composer/IO/NullIO.php
@@ -62,28 +62,28 @@ class NullIO extends BaseIO
/**
* {@inheritDoc}
*/
- public function write($messages, $newline = true)
+ public function write($messages, $newline = true, $verbosity = self::NORMAL)
{
}
/**
* {@inheritDoc}
*/
- public function writeError($messages, $newline = true)
+ public function writeError($messages, $newline = true, $verbosity = self::NORMAL)
{
}
/**
* {@inheritDoc}
*/
- public function overwrite($messages, $newline = true, $size = 80)
+ public function overwrite($messages, $newline = true, $size = 80, $verbosity = self::NORMAL)
{
}
/**
* {@inheritDoc}
*/
- public function overwriteError($messages, $newline = true, $size = 80)
+ public function overwriteError($messages, $newline = true, $size = 80, $verbosity = self::NORMAL)
{
}
diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php
index adc2dc67b..78ea92c03 100644
--- a/src/Composer/Installer.php
+++ b/src/Composer/Installer.php
@@ -529,10 +529,8 @@ class Installer
return max(1, $e->getCode());
}
- if ($this->io->isVerbose()) {
- $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies");
- $this->io->writeError("Analyzed ".$solver->getRuleSetSize()." rules to resolve dependencies");
- }
+ $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE);
+ $this->io->writeError("Analyzed ".$solver->getRuleSetSize()." rules to resolve dependencies", true, IOInterface::VERBOSE);
// force dev packages to be updated if we update or install from a (potentially new) lock
$operations = $this->processDevPackages($localRepo, $pool, $policy, $repositories, $installedRepo, $lockedRepository, $installFromLock, $withDevReqs, 'force-updates', $operations);
@@ -578,10 +576,8 @@ class Installer
&& (!$operation->getTargetPackage()->getSourceReference() || $operation->getTargetPackage()->getSourceReference() === $operation->getInitialPackage()->getSourceReference())
&& (!$operation->getTargetPackage()->getDistReference() || $operation->getTargetPackage()->getDistReference() === $operation->getInitialPackage()->getDistReference())
) {
- if ($this->io->isDebug()) {
- $this->io->writeError(' - Skipping update of '. $operation->getTargetPackage()->getPrettyName().' to the same reference-locked version');
- $this->io->writeError('');
- }
+ $this->io->writeError(' - Skipping update of '. $operation->getTargetPackage()->getPrettyName().' to the same reference-locked version', true, IOInterface::DEBUG);
+ $this->io->writeError('', true, IOInterface::DEBUG);
continue;
}
diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php
index 20a281de7..31090de00 100644
--- a/src/Composer/Installer/LibraryInstaller.php
+++ b/src/Composer/Installer/LibraryInstaller.php
@@ -17,7 +17,9 @@ use Composer\IO\IOInterface;
use Composer\Repository\InstalledRepositoryInterface;
use Composer\Package\PackageInterface;
use Composer\Util\Filesystem;
+use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
+use Composer\Util\Silencer;
/**
* Package installation manager.
@@ -130,7 +132,7 @@ class LibraryInstaller implements InstallerInterface
if (strpos($package->getName(), '/')) {
$packageVendorDir = dirname($downloadPath);
if (is_dir($packageVendorDir) && $this->filesystem->isDirEmpty($packageVendorDir)) {
- @rmdir($packageVendorDir);
+ Silencer::call('rmdir', $packageVendorDir);
}
}
}
@@ -233,14 +235,14 @@ class LibraryInstaller implements InstallerInterface
// likely leftover from a previous install, make sure
// that the target is still executable in case this
// is a fresh install of the vendor.
- @chmod($link, 0777 & ~umask());
+ Silencer::call('chmod', $link, 0777 & ~umask());
}
$this->io->writeError(' Skipped installation of bin '.$bin.' for package '.$package->getName().': name conflicts with an existing file');
continue;
}
if ($this->binCompat === "auto") {
- if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+ if (Platform::isWindows()) {
$this->installFullBinaries($binPath, $link, $bin, $package);
} else {
$this->installSymlinkBinaries($binPath, $link);
@@ -248,7 +250,7 @@ class LibraryInstaller implements InstallerInterface
} elseif ($this->binCompat === "full") {
$this->installFullBinaries($binPath, $link, $bin, $package);
}
- @chmod($link, 0777 & ~umask());
+ Silencer::call('chmod', $link, 0777 & ~umask());
}
}
@@ -298,7 +300,7 @@ class LibraryInstaller implements InstallerInterface
// attempt removing the bin dir in case it is left empty
if ((is_dir($this->binDir)) && ($this->filesystem->isDirEmpty($this->binDir))) {
- @rmdir($this->binDir);
+ Silencer::call('rmdir', $this->binDir);
}
}
diff --git a/src/Composer/Installer/PearInstaller.php b/src/Composer/Installer/PearInstaller.php
index 146e68b95..0e16fcb32 100644
--- a/src/Composer/Installer/PearInstaller.php
+++ b/src/Composer/Installer/PearInstaller.php
@@ -17,6 +17,7 @@ use Composer\Composer;
use Composer\Downloader\PearPackageExtractor;
use Composer\Repository\InstalledRepositoryInterface;
use Composer\Package\PackageInterface;
+use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
/**
@@ -53,7 +54,7 @@ class PearInstaller extends LibraryInstaller
parent::installCode($package);
parent::initializeBinDir();
- $isWindows = defined('PHP_WINDOWS_VERSION_BUILD');
+ $isWindows = Platform::isWindows();
$php_bin = $this->binDir . ($isWindows ? '/composer-php.bat' : '/composer-php');
if (!$isWindows) {
@@ -75,9 +76,7 @@ class PearInstaller extends LibraryInstaller
$pearExtractor = new PearPackageExtractor($packageArchive);
$pearExtractor->extractTo($this->getInstallPath($package), array('php' => '/', 'script' => '/bin', 'data' => '/data'), $vars);
- if ($this->io->isVerbose()) {
- $this->io->writeError(' Cleaning up');
- }
+ $this->io->writeError(' Cleaning up', true, IOInterface::VERBOSE);
$this->filesystem->unlink($packageArchive);
}
diff --git a/src/Composer/Package/AliasPackage.php b/src/Composer/Package/AliasPackage.php
index f6849a234..e161a4482 100644
--- a/src/Composer/Package/AliasPackage.php
+++ b/src/Composer/Package/AliasPackage.php
@@ -165,7 +165,7 @@ class AliasPackage extends BasePackage implements CompletePackageInterface
}
/**
- * @param Link[] $links
+ * @param Link[] $links
* @param string $linkType
*
* @return Link[]
diff --git a/src/Composer/Package/Loader/RootPackageLoader.php b/src/Composer/Package/Loader/RootPackageLoader.php
index 0fc7c49af..565ff39c4 100644
--- a/src/Composer/Package/Loader/RootPackageLoader.php
+++ b/src/Composer/Package/Loader/RootPackageLoader.php
@@ -113,6 +113,11 @@ class RootPackageLoader extends ArrayLoader
}
}
+ if (isset($links[$config['name']])) {
+ throw new \InvalidArgumentException(sprintf('Root package \'%s\' cannot require itself in its composer.json' . PHP_EOL .
+ 'Did you accidentally name your root package after an external package?', $config['name']));
+ }
+
$realPackage->setAliases($aliases);
$realPackage->setStabilityFlags($stabilityFlags);
$realPackage->setReferences($references);
diff --git a/src/Composer/Plugin/Capability/Capability.php b/src/Composer/Plugin/Capability/Capability.php
new file mode 100644
index 000000000..b12410608
--- /dev/null
+++ b/src/Composer/Plugin/Capability/Capability.php
@@ -0,0 +1,23 @@
+
+ * Jordi Boggiano
+ *
+ * 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
+{
+}
diff --git a/src/Composer/Plugin/Capable.php b/src/Composer/Plugin/Capable.php
new file mode 100644
index 000000000..48ae42250
--- /dev/null
+++ b/src/Composer/Plugin/Capable.php
@@ -0,0 +1,43 @@
+
+ * Jordi Boggiano
+ *
+ * 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();
+}
diff --git a/src/Composer/Plugin/PluginInterface.php b/src/Composer/Plugin/PluginInterface.php
index dea5828c1..a8c2b6a94 100644
--- a/src/Composer/Plugin/PluginInterface.php
+++ b/src/Composer/Plugin/PluginInterface.php
@@ -23,14 +23,14 @@ use Composer\IO\IOInterface;
interface PluginInterface
{
/**
- * Version number of the fake composer-plugin-api package
+ * Version number of the internal composer-plugin-api package
*
* @var string
*/
const PLUGIN_API_VERSION = '1.0.0';
/**
- * Apply plugin modifications to composer
+ * Apply plugin modifications to Composer
*
* @param Composer $composer
* @param IOInterface $io
diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php
index 27680907d..175a5e05b 100644
--- a/src/Composer/Plugin/PluginManager.php
+++ b/src/Composer/Plugin/PluginManager.php
@@ -23,6 +23,7 @@ use Composer\Package\PackageInterface;
use Composer\Package\Link;
use Composer\Semver\Constraint\Constraint;
use Composer\DependencyResolver\Pool;
+use Composer\Plugin\Capability\Capability;
/**
* Plugin manager
@@ -122,8 +123,11 @@ class PluginManager
$currentPluginApiVersion = $this->getPluginApiVersion();
$currentPluginApiConstraint = new Constraint('==', $this->versionParser->normalize($currentPluginApiVersion));
- if (!$requiresComposer->matches($currentPluginApiConstraint)) {
+ if ($requiresComposer->getPrettyString() === '1.0.0' && $this->getPluginApiVersion() === '1.0.0') {
+ $this->io->writeError('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).');
+ } elseif (!$requiresComposer->matches($currentPluginApiConstraint)) {
$this->io->writeError('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.');
+
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;
+ }
+ }
}
diff --git a/src/Composer/Repository/ArtifactRepository.php b/src/Composer/Repository/ArtifactRepository.php
index 81335ef7e..ece78bb1c 100644
--- a/src/Composer/Repository/ArtifactRepository.php
+++ b/src/Composer/Repository/ArtifactRepository.php
@@ -67,16 +67,12 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito
$package = $this->getComposerInformation($file);
if (!$package) {
- if ($io->isVerbose()) {
- $io->writeError("File {$file->getBasename()} doesn't seem to hold a package");
- }
+ $io->writeError("File {$file->getBasename()} doesn't seem to hold a package", true, IOInterface::VERBOSE);
continue;
}
- if ($io->isVerbose()) {
- $template = 'Found package %s (%s) in file %s';
- $io->writeError(sprintf($template, $package->getName(), $package->getPrettyVersion(), $file->getBasename()));
- }
+ $template = 'Found package %s (%s) in file %s';
+ $io->writeError(sprintf($template, $package->getName(), $package->getPrettyVersion(), $file->getBasename()), true, IOInterface::VERBOSE);
$this->addPackage($package);
}
diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php
index 6806d1c8a..f3cf20e29 100644
--- a/src/Composer/Repository/ComposerRepository.php
+++ b/src/Composer/Repository/ComposerRepository.php
@@ -747,6 +747,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
$this->io->writeError(''.$this->url.' could not be fully loaded, package information was loaded from the local cache and may be out of date');
}
$this->degradedMode = true;
+
return true;
}
}
diff --git a/src/Composer/Repository/PathRepository.php b/src/Composer/Repository/PathRepository.php
index b826ca999..7fa004eb5 100644
--- a/src/Composer/Repository/PathRepository.php
+++ b/src/Composer/Repository/PathRepository.php
@@ -113,7 +113,7 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn
parent::initialize();
foreach ($this->getUrlMatches() as $url) {
- $path = realpath($url) . '/';
+ $path = realpath($url) . DIRECTORY_SEPARATOR;
$composerFilePath = $path.'composer.json';
if (!file_exists($composerFilePath)) {
@@ -125,16 +125,16 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn
$package['dist'] = array(
'type' => 'path',
'url' => $url,
- 'reference' => '',
+ 'reference' => sha1($json),
);
if (!isset($package['version'])) {
$package['version'] = $this->versionGuesser->guessVersion($package, $path) ?: 'dev-master';
}
- if (is_dir($path.'/.git') && 0 === $this->process->execute('git log -n1 --pretty=%H', $output, $path)) {
+
+ $output = '';
+ if (is_dir($path . DIRECTORY_SEPARATOR . '.git') && 0 === $this->process->execute('git log -n1 --pretty=%H', $output, $path)) {
$package['dist']['reference'] = trim($output);
- } else {
- $package['dist']['reference'] = Locker::getContentHash($json);
}
$package = $this->loader->load($package);
@@ -153,6 +153,9 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn
*/
private function getUrlMatches()
{
- return glob($this->url, GLOB_MARK | GLOB_ONLYDIR);
+ // Ensure environment-specific path separators are normalized to URL separators
+ return array_map(function ($val) {
+ return str_replace(DIRECTORY_SEPARATOR, '/', $val);
+ }, glob($this->url, GLOB_MARK | GLOB_ONLYDIR));
}
}
diff --git a/src/Composer/Repository/PearRepository.php b/src/Composer/Repository/PearRepository.php
index 1d6d4710a..2c3e64bbe 100644
--- a/src/Composer/Repository/PearRepository.php
+++ b/src/Composer/Repository/PearRepository.php
@@ -105,9 +105,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn
try {
$normalizedVersion = $versionParser->normalize($version);
} catch (\UnexpectedValueException $e) {
- if ($this->io->isVerbose()) {
- $this->io->writeError('Could not load '.$packageDefinition->getPackageName().' '.$version.': '.$e->getMessage());
- }
+ $this->io->writeError('Could not load '.$packageDefinition->getPackageName().' '.$version.': '.$e->getMessage(), true, IOInterface::VERBOSE);
continue;
}
diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php
index 084bc2af7..833c82c20 100644
--- a/src/Composer/Repository/PlatformRepository.php
+++ b/src/Composer/Repository/PlatformRepository.php
@@ -203,6 +203,7 @@ class PlatformRepository extends ArrayRepository
if (isset($this->overrides[strtolower($package->getName())])) {
$overrider = $this->findPackage($package->getName(), '*');
$overrider->setDescription($overrider->getDescription().' (actual: '.$package->getPrettyVersion().')');
+
return;
}
parent::addPackage($package);
diff --git a/src/Composer/Repository/RepositoryManager.php b/src/Composer/Repository/RepositoryManager.php
index 7f91ac6ff..42c14106f 100644
--- a/src/Composer/Repository/RepositoryManager.php
+++ b/src/Composer/Repository/RepositoryManager.php
@@ -105,7 +105,6 @@ class RepositoryManager
$class = $this->repositoryClasses[$type];
-
$reflMethod = new \ReflectionMethod($class, '__construct');
$params = $reflMethod->getParameters();
if (isset($params[4]) && $params[4]->getClass() && $params[4]->getClass()->getName() === 'Composer\Util\RemoteFilesystem') {
diff --git a/src/Composer/Repository/Vcs/GitBitbucketDriver.php b/src/Composer/Repository/Vcs/GitBitbucketDriver.php
index 0f0a57c47..7a2781ecb 100644
--- a/src/Composer/Repository/Vcs/GitBitbucketDriver.php
+++ b/src/Composer/Repository/Vcs/GitBitbucketDriver.php
@@ -160,9 +160,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface
}
if (!extension_loaded('openssl')) {
- if ($io->isVerbose()) {
- $io->writeError('Skipping Bitbucket git driver for '.$url.' because the OpenSSL PHP extension is missing.');
- }
+ $io->writeError('Skipping Bitbucket git driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE);
return false;
}
diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php
index cdd9df89f..4a4e1fcea 100644
--- a/src/Composer/Repository/Vcs/GitHubDriver.php
+++ b/src/Composer/Repository/Vcs/GitHubDriver.php
@@ -268,9 +268,7 @@ class GitHubDriver extends VcsDriver
}
if (!extension_loaded('openssl')) {
- if ($io->isVerbose()) {
- $io->writeError('Skipping GitHub driver for '.$url.' because the OpenSSL PHP extension is missing.');
- }
+ $io->writeError('Skipping GitHub driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE);
return false;
}
diff --git a/src/Composer/Repository/Vcs/GitLabDriver.php b/src/Composer/Repository/Vcs/GitLabDriver.php
index fa13f952c..8642c2d42 100644
--- a/src/Composer/Repository/Vcs/GitLabDriver.php
+++ b/src/Composer/Repository/Vcs/GitLabDriver.php
@@ -367,9 +367,7 @@ class GitLabDriver extends VcsDriver
}
if ('https' === $scheme && !extension_loaded('openssl')) {
- if ($io->isVerbose()) {
- $io->write('Skipping GitLab driver for '.$url.' because the OpenSSL PHP extension is missing.');
- }
+ $io->writeError('Skipping GitLab driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE);
return false;
}
diff --git a/src/Composer/Repository/Vcs/HgBitbucketDriver.php b/src/Composer/Repository/Vcs/HgBitbucketDriver.php
index 3beeee440..eb6808601 100644
--- a/src/Composer/Repository/Vcs/HgBitbucketDriver.php
+++ b/src/Composer/Repository/Vcs/HgBitbucketDriver.php
@@ -170,9 +170,7 @@ class HgBitbucketDriver extends VcsDriver
}
if (!extension_loaded('openssl')) {
- if ($io->isVerbose()) {
- $io->writeError('Skipping Bitbucket hg driver for '.$url.' because the OpenSSL PHP extension is missing.');
- }
+ $io->writeError('Skipping Bitbucket hg driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE);
return false;
}
diff --git a/src/Composer/Util/ConfigValidator.php b/src/Composer/Util/ConfigValidator.php
index cb5706c88..f36ff8e6f 100644
--- a/src/Composer/Util/ConfigValidator.php
+++ b/src/Composer/Util/ConfigValidator.php
@@ -19,7 +19,6 @@ use Composer\Json\JsonValidationException;
use Composer\IO\IOInterface;
use Composer\Json\JsonFile;
use Composer\Spdx\SpdxLicenses;
-use Composer\Factory;
/**
* Validates a composer configuration.
diff --git a/src/Composer/Util/ErrorHandler.php b/src/Composer/Util/ErrorHandler.php
index 399491f8c..ddb4b570b 100644
--- a/src/Composer/Util/ErrorHandler.php
+++ b/src/Composer/Util/ErrorHandler.php
@@ -36,8 +36,8 @@ class ErrorHandler
*/
public static function handle($level, $message, $file, $line)
{
- // respect error_reporting being disabled
- if (!error_reporting()) {
+ // error code is not included in error_reporting
+ if (!(error_reporting() & $level)) {
return;
}
@@ -73,6 +73,7 @@ class ErrorHandler
public static function register(IOInterface $io = null)
{
set_error_handler(array(__CLASS__, 'handle'));
+ error_reporting(E_ALL | E_STRICT);
self::$io = $io;
}
}
diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php
index f7d812de8..e29a60673 100644
--- a/src/Composer/Util/Filesystem.php
+++ b/src/Composer/Util/Filesystem.php
@@ -110,7 +110,7 @@ class Filesystem
return $this->removeDirectoryPhp($directory);
}
- if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+ if (Platform::isWindows()) {
$cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape(realpath($directory)));
} else {
$cmd = sprintf('rm -rf %s', ProcessExecutor::escape($directory));
@@ -181,10 +181,10 @@ class Filesystem
{
if (!@$this->unlinkImplementation($path)) {
// retry after a bit on windows since it tends to be touchy with mass removals
- if (!defined('PHP_WINDOWS_VERSION_BUILD') || (usleep(350000) && !@$this->unlinkImplementation($path))) {
+ if (!Platform::isWindows() || (usleep(350000) && !@$this->unlinkImplementation($path))) {
$error = error_get_last();
$message = 'Could not delete '.$path.': ' . @$error['message'];
- if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+ if (Platform::isWindows()) {
$message .= "\nThis can be due to an antivirus or the Windows Search Indexer locking the file while they are analyzed";
}
@@ -206,10 +206,10 @@ class Filesystem
{
if (!@rmdir($path)) {
// retry after a bit on windows since it tends to be touchy with mass removals
- if (!defined('PHP_WINDOWS_VERSION_BUILD') || (usleep(350000) && !@rmdir($path))) {
+ if (!Platform::isWindows() || (usleep(350000) && !@rmdir($path))) {
$error = error_get_last();
$message = 'Could not delete '.$path.': ' . @$error['message'];
- if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+ if (Platform::isWindows()) {
$message .= "\nThis can be due to an antivirus or the Windows Search Indexer locking the file while they are analyzed";
}
@@ -264,7 +264,7 @@ class Filesystem
return $this->copyThenRemove($source, $target);
}
- if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+ if (Platform::isWindows()) {
// Try to copy & delete - this is a workaround for random "Access denied" errors.
$command = sprintf('xcopy %s %s /E /I /Q /Y', ProcessExecutor::escape($source), ProcessExecutor::escape($target));
$result = $this->processExecutor->execute($command, $output);
@@ -460,7 +460,7 @@ class Filesystem
public static function getPlatformPath($path)
{
- if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+ if (Platform::isWindows()) {
$path = preg_replace('{^(?:file:///([a-z])/)}i', 'file://$1:/', $path);
}
@@ -498,7 +498,7 @@ class Filesystem
*/
private function unlinkImplementation($path)
{
- if (defined('PHP_WINDOWS_VERSION_BUILD') && is_dir($path) && is_link($path)) {
+ if (Platform::isWindows() && is_dir($path) && is_link($path)) {
return rmdir($path);
}
diff --git a/src/Composer/Util/Keys.php b/src/Composer/Util/Keys.php
new file mode 100644
index 000000000..4afa204cd
--- /dev/null
+++ b/src/Composer/Util/Keys.php
@@ -0,0 +1,36 @@
+
+ * Jordi Boggiano
+ *
+ * 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
+ */
+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),
+ ));
+ }
+}
diff --git a/src/Composer/Util/Perforce.php b/src/Composer/Util/Perforce.php
index c1eaeebe9..b57a64a1f 100644
--- a/src/Composer/Util/Perforce.php
+++ b/src/Composer/Util/Perforce.php
@@ -51,10 +51,7 @@ class Perforce
public static function create($repoConfig, $port, $path, ProcessExecutor $process, IOInterface $io)
{
- $isWindows = defined('PHP_WINDOWS_VERSION_BUILD');
- $perforce = new Perforce($repoConfig, $port, $path, $process, $isWindows, $io);
-
- return $perforce;
+ return new Perforce($repoConfig, $port, $path, $process, Platform::isWindows(), $io);
}
public static function checkServerExists($url, ProcessExecutor $processExecutor)
diff --git a/src/Composer/Util/Platform.php b/src/Composer/Util/Platform.php
new file mode 100644
index 000000000..eafb88b7a
--- /dev/null
+++ b/src/Composer/Util/Platform.php
@@ -0,0 +1,28 @@
+
+ * Jordi Boggiano
+ *
+ * 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
+ */
+class Platform
+{
+ /**
+ * @return bool Whether the host machine is running a Windows OS
+ */
+ public static function isWindows()
+ {
+ return defined('PHP_WINDOWS_VERSION_BUILD');
+ }
+}
diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php
index f9499f09f..6b778e5eb 100644
--- a/src/Composer/Util/ProcessExecutor.php
+++ b/src/Composer/Util/ProcessExecutor.php
@@ -50,7 +50,7 @@ class ProcessExecutor
// make sure that null translate to the proper directory in case the dir is a symlink
// and we call a git command, because msysgit does not handle symlinks properly
- if (null === $cwd && defined('PHP_WINDOWS_VERSION_BUILD') && false !== strpos($command, 'git') && getcwd()) {
+ if (null === $cwd && Platform::isWindows() && false !== strpos($command, 'git') && getcwd()) {
$cwd = realpath(getcwd());
}
diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php
index 4754e304b..f4c5f3150 100644
--- a/src/Composer/Util/RemoteFilesystem.php
+++ b/src/Composer/Util/RemoteFilesystem.php
@@ -33,11 +33,14 @@ class RemoteFilesystem
private $progress;
private $lastProgress;
private $options = array();
+ private $peerCertificateMap = array();
private $disableTls = false;
private $retryAuthFailure;
private $lastHeaders;
private $storeAuth;
private $degradedMode = false;
+ private $redirects;
+ private $maxRedirects = 20;
/**
* Constructor.
@@ -54,15 +57,7 @@ class RemoteFilesystem
// Setup TLS options
// The cafile option can be set via config.json
if ($disableTls === false) {
- $this->options = $this->getTlsDefaults();
- if (isset($options['ssl']['cafile'])
- && (
- !is_readable($options['ssl']['cafile'])
- || !$this->validateCaFile($options['ssl']['cafile'])
- )
- ) {
- throw new TransportException('The configured cafile was not valid or could not be read.');
- }
+ $this->options = $this->getTlsDefaults($options);
} else {
$this->disableTls = true;
}
@@ -139,8 +134,8 @@ class RemoteFilesystem
}
/**
- * @param array $headers array of returned headers like from getLastHeaders()
- * @param string $name header name (case insensitive)
+ * @param array $headers array of returned headers like from getLastHeaders()
+ * @param string $name header name (case insensitive)
* @return string|null
*/
public function findHeaderValue(array $headers, $name)
@@ -160,7 +155,7 @@ class RemoteFilesystem
}
/**
- * @param array $headers array of returned headers like from getLastHeaders()
+ * @param array $headers array of returned headers like from getLastHeaders()
* @return int|null
*/
public function findStatusCode(array $headers)
@@ -206,24 +201,34 @@ class RemoteFilesystem
$this->lastProgress = null;
$this->retryAuthFailure = true;
$this->lastHeaders = array();
+ $this->redirects = 1; // The first request counts.
// capture username/password from URL if there is one
if (preg_match('{^https?://(.+):(.+)@([^/]+)}i', $fileUrl, $match)) {
$this->io->setAuthentication($originUrl, urldecode($match[1]), urldecode($match[2]));
}
- if (isset($additionalOptions['retry-auth-failure'])) {
- $this->retryAuthFailure = (bool) $additionalOptions['retry-auth-failure'];
+ $tempAdditionalOptions = $additionalOptions;
+ if (isset($tempAdditionalOptions['retry-auth-failure'])) {
+ $this->retryAuthFailure = (bool) $tempAdditionalOptions['retry-auth-failure'];
- unset($additionalOptions['retry-auth-failure']);
+ unset($tempAdditionalOptions['retry-auth-failure']);
}
- $options = $this->getOptionsForUrl($originUrl, $additionalOptions);
+ $isRedirect = false;
+ if (isset($tempAdditionalOptions['redirects'])) {
+ $this->redirects = $tempAdditionalOptions['redirects'];
+ $isRedirect = true;
- if ($this->io->isDebug()) {
- $this->io->writeError((substr($fileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $fileUrl);
+ unset($tempAdditionalOptions['redirects']);
}
+ $options = $this->getOptionsForUrl($originUrl, $tempAdditionalOptions);
+ unset($tempAdditionalOptions);
+ $userlandFollow = isset($options['http']['follow_location']) && !$options['http']['follow_location'];
+
+ $this->io->writeError((substr($fileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $fileUrl, true, IOInterface::DEBUG);
+
if (isset($options['github-token'])) {
$fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['github-token'];
unset($options['github-token']);
@@ -245,7 +250,7 @@ class RemoteFilesystem
$ctx = StreamContextFactory::getContext($fileUrl, $options, array('notification' => array($this, 'callbackGet')));
- if ($this->progress) {
+ if ($this->progress && !$isRedirect) {
$this->io->writeError(" Downloading: Connecting...", 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: 100%");
}
@@ -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(
+ 'Your version of PHP, %s, is affected by CVE-2013-6420 and cannot safely perform certificate validation, we strongly suggest you upgrade.',
+ 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;
- } else {
- $host = parse_url($this->fileUrl, PHP_URL_HOST);
- }
+ if ($this->disableTls === false && PHP_VERSION_ID < 50600 && !stream_is_local($this->fileUrl)) {
+ $host = parse_url($this->fileUrl, PHP_URL_HOST);
- if ($host === 'github.com' || $host === 'api.github.com') {
- $host = '*.github.com';
+ 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 {
+ // PHP < 5.3.4 does not support follow_location, for those people
+ // do some really nasty hard coded transformations. These will
+ // still breakdown if the site redirects to a domain we don't
+ // expect.
+
+ if ($host === 'github.com' || $host === 'api.github.com') {
+ $host = '*.github.com';
+ }
}
$tlsOptions['ssl']['CN_match'] = $host;
$tlsOptions['ssl']['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 %s as CN for subjectAltName enabled host %s',
+ $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);
+ if (preg_match('{^phar://}', $result)) {
+ $hash = hash_file('sha256', $result);
+ $targetPath = rtrim(sys_get_temp_dir(), '\\/') . '/composer-cacert-' . $hash . '.pem';
- $options['ssl']['cafile'] = $targetPath;
- } else {
- if (is_dir($result)) {
- $options['ssl']['capath'] = $result;
- } elseif ($result) {
- $options['ssl']['cafile'] = $result;
- }
+ 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
- *
- * 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
+ *
+ * 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(
+ 'Your version of PHP, %s, is affected by CVE-2013-6420 and cannot safely perform certificate validation, we strongly suggest you upgrade.',
+ 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;
}
}
diff --git a/src/Composer/Util/Silencer.php b/src/Composer/Util/Silencer.php
new file mode 100644
index 000000000..03cfff430
--- /dev/null
+++ b/src/Composer/Util/Silencer.php
@@ -0,0 +1,77 @@
+
+ * Jordi Boggiano
+ *
+ * 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
+ */
+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;
+ }
+ }
+}
diff --git a/src/Composer/Util/TlsHelper.php b/src/Composer/Util/TlsHelper.php
new file mode 100644
index 000000000..721e93825
--- /dev/null
+++ b/src/Composer/Util/TlsHelper.php
@@ -0,0 +1,289 @@
+
+ * Jordi Boggiano
+ *
+ * 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
+ */
+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
+ *
+ * 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);
+ };
+ }
+ }
+}
diff --git a/tests/Composer/Test/AllFunctionalTest.php b/tests/Composer/Test/AllFunctionalTest.php
index df8ddf185..7ef3aa0af 100644
--- a/tests/Composer/Test/AllFunctionalTest.php
+++ b/tests/Composer/Test/AllFunctionalTest.php
@@ -12,14 +12,15 @@
namespace Composer\Test;
-use Symfony\Component\Process\Process;
+use Composer\TestCase;
use Composer\Util\Filesystem;
use Symfony\Component\Finder\Finder;
+use Symfony\Component\Process\Process;
/**
* @group slow
*/
-class AllFunctionalTest extends \PHPUnit_Framework_TestCase
+class AllFunctionalTest extends TestCase
{
protected $oldcwd;
protected $oldenv;
@@ -29,17 +30,21 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase
public function setUp()
{
$this->oldcwd = getcwd();
+
chdir(__DIR__.'/Fixtures/functional');
}
public function tearDown()
{
chdir($this->oldcwd);
+
$fs = new Filesystem;
+
if ($this->testDir) {
$fs->removeDirectory($this->testDir);
$this->testDir = null;
}
+
if ($this->oldenv) {
$fs->removeDirectory(getenv('COMPOSER_HOME'));
$_SERVER['COMPOSER_HOME'] = $this->oldenv;
@@ -50,7 +55,7 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase
public static function setUpBeforeClass()
{
- self::$pharPath = sys_get_temp_dir().'/composer-phar-test/composer.phar';
+ self::$pharPath = self::getUniqueTmpDirectory() . '/composer.phar';
}
public static function tearDownAfterClass()
@@ -66,9 +71,7 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase
}
$target = dirname(self::$pharPath);
- $fs = new Filesystem;
- $fs->removeDirectory($target);
- $fs->ensureDirectoryExists($target);
+ $fs = new Filesystem();
chdir($target);
$it = new \RecursiveDirectoryIterator(__DIR__.'/../../../', \RecursiveDirectoryIterator::SKIP_DOTS);
@@ -85,9 +88,11 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase
$proc = new Process('php '.escapeshellarg('./bin/compile'), $target);
$exitcode = $proc->run();
+
if ($exitcode !== 0 || trim($proc->getOutput())) {
$this->fail($proc->getOutput());
}
+
$this->assertTrue(file_exists(self::$pharPath));
}
@@ -140,7 +145,7 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase
$data = array();
$section = null;
- $testDir = sys_get_temp_dir().'/composer_functional_test'.uniqid(mt_rand(), true);
+ $testDir = self::getUniqueTmpDirectory();
$this->testDir = $testDir;
$varRegex = '#%([a-zA-Z_-]+)%#';
$variableReplacer = function ($match) use (&$data, $testDir) {
diff --git a/tests/Composer/Test/ApplicationTest.php b/tests/Composer/Test/ApplicationTest.php
index 68d17d3f6..e7ab59c78 100644
--- a/tests/Composer/Test/ApplicationTest.php
+++ b/tests/Composer/Test/ApplicationTest.php
@@ -14,6 +14,7 @@ namespace Composer\Test;
use Composer\Console\Application;
use Composer\TestCase;
+use Symfony\Component\Console\Output\OutputInterface;
class ApplicationTest extends TestCase
{
@@ -30,11 +31,19 @@ class ApplicationTest extends TestCase
$index = 0;
if (extension_loaded('xdebug')) {
+ $outputMock->expects($this->at($index++))
+ ->method("getVerbosity")
+ ->willReturn(OutputInterface::VERBOSITY_NORMAL);
+
$outputMock->expects($this->at($index++))
->method("write")
->with($this->equalTo('You are running composer with xdebug enabled. This has a major impact on runtime performance. See https://getcomposer.org/xdebug'));
}
+ $outputMock->expects($this->at($index++))
+ ->method("getVerbosity")
+ ->willReturn(OutputInterface::VERBOSITY_NORMAL);
+
$outputMock->expects($this->at($index++))
->method("write")
->with($this->equalTo(sprintf('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.', $_SERVER['PHP_SELF'])));
diff --git a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php
index 502a483ab..07706c5d2 100644
--- a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php
+++ b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php
@@ -88,8 +88,7 @@ class AutoloadGeneratorTest extends TestCase
$this->fs = new Filesystem;
$that = $this;
- $this->workingDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest-'.md5(uniqid('', true));
- $this->fs->ensureDirectoryExists($this->workingDir);
+ $this->workingDir = $this->getUniqueTmpDirectory();
$this->vendorDir = $this->workingDir.DIRECTORY_SEPARATOR.'composer-test-autoload';
$this->ensureDirectoryExistsAndClear($this->vendorDir);
@@ -144,6 +143,7 @@ class AutoloadGeneratorTest extends TestCase
if (is_dir($this->workingDir)) {
$this->fs->removeDirectory($this->workingDir);
}
+
if (is_dir($this->vendorDir)) {
$this->fs->removeDirectory($this->vendorDir);
}
diff --git a/tests/Composer/Test/Autoload/ClassMapGeneratorTest.php b/tests/Composer/Test/Autoload/ClassMapGeneratorTest.php
index 3b703d8f3..cd3d43260 100644
--- a/tests/Composer/Test/Autoload/ClassMapGeneratorTest.php
+++ b/tests/Composer/Test/Autoload/ClassMapGeneratorTest.php
@@ -19,10 +19,11 @@
namespace Composer\Test\Autoload;
use Composer\Autoload\ClassMapGenerator;
+use Composer\TestCase;
use Symfony\Component\Finder\Finder;
use Composer\Util\Filesystem;
-class ClassMapGeneratorTest extends \PHPUnit_Framework_TestCase
+class ClassMapGeneratorTest extends TestCase
{
/**
* @dataProvider getTestCreateMapTests
@@ -127,10 +128,8 @@ class ClassMapGeneratorTest extends \PHPUnit_Framework_TestCase
{
$this->checkIfFinderIsAvailable();
- $tempDir = sys_get_temp_dir().'/ComposerTestAmbiguousRefs';
- if (!is_dir($tempDir.'/other')) {
- mkdir($tempDir.'/other', 0777, true);
- }
+ $tempDir = $this->getUniqueTmpDirectory();
+ $this->ensureDirectoryExistsAndClear($tempDir.'/other');
$finder = new Finder();
$finder->files()->in($tempDir);
@@ -171,13 +170,9 @@ class ClassMapGeneratorTest extends \PHPUnit_Framework_TestCase
*/
public function testUnambiguousReference()
{
- $tempDir = sys_get_temp_dir().'/ComposerTestUnambiguousRefs';
- if (!is_dir($tempDir)) {
- mkdir($tempDir, 0777, true);
- }
+ $tempDir = $this->getUniqueTmpDirectory();
file_put_contents($tempDir.'/A.php', "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');
diff --git a/tests/Composer/Test/Config/Fixtures/config/config-with-exampletld-repository-and-options.json b/tests/Composer/Test/Config/Fixtures/config/config-with-exampletld-repository-and-options.json
new file mode 100644
index 000000000..c978851c6
--- /dev/null
+++ b/tests/Composer/Test/Config/Fixtures/config/config-with-exampletld-repository-and-options.json
@@ -0,0 +1,15 @@
+{
+ "name": "my-vend/my-app",
+ "license": "MIT",
+ "repositories": {
+ "example_tld": {
+ "type": "composer",
+ "url": "https://example.tld",
+ "options": {
+ "ssl": {
+ "local_cert": "/home/composer/.ssl/composer.pem"
+ }
+ }
+ }
+ }
+}
diff --git a/tests/Composer/Test/Config/JsonConfigSourceTest.php b/tests/Composer/Test/Config/JsonConfigSourceTest.php
index 529532263..e558932c2 100644
--- a/tests/Composer/Test/Config/JsonConfigSourceTest.php
+++ b/tests/Composer/Test/Config/JsonConfigSourceTest.php
@@ -14,9 +14,10 @@ namespace Composer\Test\Json;
use Composer\Config\JsonConfigSource;
use Composer\Json\JsonFile;
+use Composer\TestCase;
use Composer\Util\Filesystem;
-class JsonConfigSourceTest extends \PHPUnit_Framework_TestCase
+class JsonConfigSourceTest extends TestCase
{
/** @var Filesystem */
private $fs;
@@ -31,8 +32,7 @@ class JsonConfigSourceTest extends \PHPUnit_Framework_TestCase
protected function setUp()
{
$this->fs = new Filesystem;
- $this->workingDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest';
- $this->fs->ensureDirectoryExists($this->workingDir);
+ $this->workingDir = $this->getUniqueTmpDirectory();
}
protected function tearDown()
@@ -52,6 +52,24 @@ class JsonConfigSourceTest extends \PHPUnit_Framework_TestCase
$this->assertFileEquals($this->fixturePath('config/config-with-exampletld-repository.json'), $config);
}
+ public function testAddRepositoryWithOptions()
+ {
+ $config = $this->workingDir.'/composer.json';
+ copy($this->fixturePath('composer-repositories.json'), $config);
+ $jsonConfigSource = new JsonConfigSource(new JsonFile($config));
+ $jsonConfigSource->addRepository('example_tld', array(
+ 'type' => 'composer',
+ 'url' => 'https://example.tld',
+ 'options' => array(
+ 'ssl' => array(
+ 'local_cert' => '/home/composer/.ssl/composer.pem',
+ ),
+ ),
+ ));
+
+ $this->assertFileEquals($this->fixturePath('config/config-with-exampletld-repository-and-options.json'), $config);
+ }
+
public function testRemoveRepository()
{
$config = $this->workingDir.'/composer.json';
diff --git a/tests/Composer/Test/ConfigTest.php b/tests/Composer/Test/ConfigTest.php
index 88d8a15d3..ca3e54ce7 100644
--- a/tests/Composer/Test/ConfigTest.php
+++ b/tests/Composer/Test/ConfigTest.php
@@ -148,6 +148,16 @@ class ConfigTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('/baz', $config->get('cache-dir'));
}
+ public function testStreamWrapperDirs()
+ {
+ $config = new Config(false, '/foo/bar');
+ $config->merge(array('config' => array(
+ 'cache-dir' => 's3://baz/',
+ )));
+
+ $this->assertEquals('s3://baz', $config->get('cache-dir'));
+ }
+
public function testFetchingRelativePaths()
{
$config = new Config(false, '/foo/bar');
diff --git a/tests/Composer/Test/DefaultConfigTest.php b/tests/Composer/Test/DefaultConfigTest.php
index 4cca5025a..74a74aecd 100644
--- a/tests/Composer/Test/DefaultConfigTest.php
+++ b/tests/Composer/Test/DefaultConfigTest.php
@@ -24,5 +24,4 @@ class DefaultConfigTest extends \PHPUnit_Framework_TestCase
$config = new Config;
$this->assertFalse($config->get('disable-tls'));
}
-
-}
\ No newline at end of file
+}
diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php
index ed62e3d79..63de2973f 100644
--- a/tests/Composer/Test/DependencyResolver/SolverTest.php
+++ b/tests/Composer/Test/DependencyResolver/SolverTest.php
@@ -709,7 +709,7 @@ class SolverTest extends TestCase
$msg .= "Potential causes:\n";
$msg .= " - A typo in the package name\n";
$msg .= " - The package is not available in a stable-enough version according to your minimum-stability setting\n";
- $msg .= " see for more details.\n\n";
+ $msg .= " see for more details.\n\n";
$msg .= "Read for further common problems.";
$this->assertEquals($msg, $e->getMessage());
}
diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php
index f0578f6be..9b9f7b671 100644
--- a/tests/Composer/Test/Downloader/FileDownloaderTest.php
+++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php
@@ -13,9 +13,10 @@
namespace Composer\Test\Downloader;
use Composer\Downloader\FileDownloader;
+use Composer\TestCase;
use Composer\Util\Filesystem;
-class FileDownloaderTest extends \PHPUnit_Framework_TestCase
+class FileDownloaderTest extends TestCase
{
protected function getDownloader($io = null, $config = null, $eventDispatcher = null, $cache = null, $rfs = null, $filesystem = null)
{
@@ -53,9 +54,9 @@ class FileDownloaderTest extends \PHPUnit_Framework_TestCase
->will($this->returnValue(array('url')))
;
- $path = tempnam(sys_get_temp_dir(), 'c');
-
+ $path = tempnam($this->getUniqueTmpDirectory(), 'c');
$downloader = $this->getDownloader();
+
try {
$downloader->download($packageMock, $path);
$this->fail();
@@ -102,10 +103,7 @@ class FileDownloaderTest extends \PHPUnit_Framework_TestCase
->will($this->returnValue(array()))
;
- do {
- $path = sys_get_temp_dir().'/'.md5(time().mt_rand());
- } while (file_exists($path));
-
+ $path = $this->getUniqueTmpDirectory();
$ioMock = $this->getMock('Composer\IO\IOInterface');
$ioMock->expects($this->any())
->method('write')
@@ -187,14 +185,9 @@ class FileDownloaderTest extends \PHPUnit_Framework_TestCase
;
$filesystem = $this->getMock('Composer\Util\Filesystem');
- do {
- $path = sys_get_temp_dir().'/'.md5(time().mt_rand());
- } while (file_exists($path));
-
+ $path = $this->getUniqueTmpDirectory();
$downloader = $this->getDownloader(null, null, null, null, null, $filesystem);
-
// make sure the file expected to be downloaded is on disk already
- mkdir($path, 0777, true);
touch($path.'/script.js');
try {
diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php
index f0b6699e5..9c851fe9a 100644
--- a/tests/Composer/Test/Downloader/GitDownloaderTest.php
+++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php
@@ -14,9 +14,11 @@ namespace Composer\Test\Downloader;
use Composer\Downloader\GitDownloader;
use Composer\Config;
+use Composer\TestCase;
use Composer\Util\Filesystem;
+use Composer\Util\Platform;
-class GitDownloaderTest extends \PHPUnit_Framework_TestCase
+class GitDownloaderTest extends TestCase
{
/** @var Filesystem */
private $fs;
@@ -26,7 +28,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase
protected function setUp()
{
$this->fs = new Filesystem;
- $this->workingDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest-'.md5(uniqid('', true));
+ $this->workingDir = $this->getUniqueTmpDirectory();
}
protected function tearDown()
@@ -317,7 +319,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase
->method('execute')
->with($this->equalTo($expectedGitUpdateCommand))
->will($this->returnValue(1));
-
+
$this->fs->ensureDirectoryExists($this->workingDir.'/.git');
$downloader = $this->getDownloaderMock(null, new Config(), $processExecutor);
$downloader->update($packageMock, $packageMock, $this->workingDir);
@@ -352,7 +354,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase
private function winCompat($cmd)
{
- if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+ if (Platform::isWindows()) {
$cmd = str_replace('cd ', 'cd /D ', $cmd);
$cmd = str_replace('composerPath', getcwd().'/composerPath', $cmd);
diff --git a/tests/Composer/Test/Downloader/HgDownloaderTest.php b/tests/Composer/Test/Downloader/HgDownloaderTest.php
index b2dee627b..6b660e383 100644
--- a/tests/Composer/Test/Downloader/HgDownloaderTest.php
+++ b/tests/Composer/Test/Downloader/HgDownloaderTest.php
@@ -13,16 +13,18 @@
namespace Composer\Test\Downloader;
use Composer\Downloader\HgDownloader;
+use Composer\TestCase;
use Composer\Util\Filesystem;
+use Composer\Util\Platform;
-class HgDownloaderTest extends \PHPUnit_Framework_TestCase
+class HgDownloaderTest extends TestCase
{
/** @var string */
private $workingDir;
protected function setUp()
{
- $this->workingDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest-'.md5(uniqid('', true));
+ $this->workingDir = $this->getUniqueTmpDirectory();
}
protected function tearDown()
@@ -155,10 +157,6 @@ class HgDownloaderTest extends \PHPUnit_Framework_TestCase
private function getCmd($cmd)
{
- if (defined('PHP_WINDOWS_VERSION_BUILD')) {
- return strtr($cmd, "'", '"');
- }
-
- return $cmd;
+ return Platform::isWindows() ? strtr($cmd, "'", '"') : $cmd;
}
}
diff --git a/tests/Composer/Test/Downloader/PearPackageExtractorTest.php b/tests/Composer/Test/Downloader/PearPackageExtractorTest.php
index 10ac27955..92004d0f1 100644
--- a/tests/Composer/Test/Downloader/PearPackageExtractorTest.php
+++ b/tests/Composer/Test/Downloader/PearPackageExtractorTest.php
@@ -13,8 +13,9 @@
namespace Composer\Test\Downloader;
use Composer\Downloader\PearPackageExtractor;
+use Composer\TestCase;
-class PearPackageExtractorTest extends \PHPUnit_Framework_TestCase
+class PearPackageExtractorTest extends TestCase
{
public function testShouldExtractPackage_1_0()
{
@@ -122,7 +123,7 @@ class PearPackageExtractorTest extends \PHPUnit_Framework_TestCase
public function testShouldPerformReplacements()
{
- $from = tempnam(sys_get_temp_dir(), 'pear-extract');
+ $from = tempnam($this->getUniqueTmpDirectory(), 'pear-extract');
$to = $from.'-to';
$original = 'replaced: @placeholder@; not replaced: @another@; replaced again: @placeholder@';
diff --git a/tests/Composer/Test/Downloader/PerforceDownloaderTest.php b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php
index fc4297633..2b8105e65 100644
--- a/tests/Composer/Test/Downloader/PerforceDownloaderTest.php
+++ b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php
@@ -16,12 +16,13 @@ use Composer\Downloader\PerforceDownloader;
use Composer\Config;
use Composer\Repository\VcsRepository;
use Composer\IO\IOInterface;
+use Composer\TestCase;
use Composer\Util\Filesystem;
/**
* @author Matt Whittom
*/
-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();
diff --git a/tests/Composer/Test/Downloader/XzDownloaderTest.php b/tests/Composer/Test/Downloader/XzDownloaderTest.php
index a71516821..d8e77a2cb 100644
--- a/tests/Composer/Test/Downloader/XzDownloaderTest.php
+++ b/tests/Composer/Test/Downloader/XzDownloaderTest.php
@@ -13,10 +13,12 @@
namespace Composer\Test\Downloader;
use Composer\Downloader\XzDownloader;
+use Composer\TestCase;
use Composer\Util\Filesystem;
+use Composer\Util\Platform;
use Composer\Util\RemoteFilesystem;
-class XzDownloaderTest extends \PHPUnit_Framework_TestCase
+class XzDownloaderTest extends TestCase
{
/**
* @var Filesystem
@@ -30,10 +32,10 @@ class XzDownloaderTest extends \PHPUnit_Framework_TestCase
public function setUp()
{
- if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+ if (Platform::isWindows()) {
$this->markTestSkipped('Skip test on Windows');
}
- $this->testDir = sys_get_temp_dir().'/composer-xz-test-vendor';
+ $this->testDir = $this->getUniqueTmpDirectory();
}
public function tearDown()
@@ -67,7 +69,7 @@ class XzDownloaderTest extends \PHPUnit_Framework_TestCase
$downloader = new XzDownloader($io, $config, null, null, null, new RemoteFilesystem($io));
try {
- $downloader->download($packageMock, sys_get_temp_dir().'/composer-xz-test');
+ $downloader->download($packageMock, $this->getUniqueTmpDirectory());
$this->fail('Download of invalid tarball should throw an exception');
} catch (\RuntimeException $e) {
$this->assertContains('File format not recognized', $e->getMessage());
diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php
index cb5a56569..f70d9e44c 100644
--- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php
+++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php
@@ -13,11 +13,11 @@
namespace Composer\Test\Downloader;
use Composer\Downloader\ZipDownloader;
+use Composer\TestCase;
use Composer\Util\Filesystem;
-class ZipDownloaderTest extends \PHPUnit_Framework_TestCase
+class ZipDownloaderTest extends TestCase
{
-
/**
* @var string
*/
@@ -28,7 +28,8 @@ class ZipDownloaderTest extends \PHPUnit_Framework_TestCase
if (!class_exists('ZipArchive')) {
$this->markTestSkipped('zip extension missing');
}
- $this->testDir = sys_get_temp_dir().'/composer-zip-test-vendor';
+
+ $this->testDir = $this->getUniqueTmpDirectory();
}
public function tearDown()
@@ -64,6 +65,10 @@ class ZipDownloaderTest extends \PHPUnit_Framework_TestCase
->with('cafile')
->will($this->returnValue(null));
$config->expects($this->at(2))
+ ->method('get')
+ ->with('capath')
+ ->will($this->returnValue(null));
+ $config->expects($this->at(3))
->method('get')
->with('vendor-dir')
->will($this->returnValue($this->testDir));
diff --git a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php
index 2dd9f8a4a..e0c1fa45e 100644
--- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php
+++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php
@@ -15,9 +15,11 @@ namespace Composer\Test\EventDispatcher;
use Composer\EventDispatcher\Event;
use Composer\Installer\InstallerEvents;
use Composer\TestCase;
+use Composer\IO\BufferIO;
use Composer\Script\ScriptEvents;
use Composer\Script\CommandEvent;
use Composer\Util\ProcessExecutor;
+use Symfony\Component\Console\Output\OutputInterface;
class EventDispatcherTest extends TestCase
{
@@ -101,7 +103,7 @@ class EventDispatcherTest extends TestCase
$dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
->setConstructorArgs(array(
$this->getMock('Composer\Composer'),
- $io = $this->getMock('Composer\IO\IOInterface'),
+ $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE),
$process,
))
->setMethods(array(
@@ -123,23 +125,12 @@ class EventDispatcherTest extends TestCase
->method('getListeners')
->will($this->returnValue($listeners));
- $io->expects($this->any())
- ->method('isVerbose')
- ->willReturn(1);
-
- $io->expects($this->at(1))
- ->method('writeError')
- ->with($this->equalTo('> post-install-cmd: echo -n foo'));
-
- $io->expects($this->at(3))
- ->method('writeError')
- ->with($this->equalTo('> post-install-cmd: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'));
-
- $io->expects($this->at(5))
- ->method('writeError')
- ->with($this->equalTo('> post-install-cmd: echo -n bar'));
-
$dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false);
+
+ $expected = '> post-install-cmd: echo -n foo'.PHP_EOL.
+ '> post-install-cmd: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'.PHP_EOL.
+ '> post-install-cmd: echo -n bar'.PHP_EOL;
+ $this->assertEquals($expected, $io->getOutput());
}
public function testDispatcherCanExecuteComposerScriptGroups()
@@ -148,7 +139,7 @@ class EventDispatcherTest extends TestCase
$dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
->setConstructorArgs(array(
$composer = $this->getMock('Composer\Composer'),
- $io = $this->getMock('Composer\IO\IOInterface'),
+ $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE),
$process,
))
->setMethods(array(
@@ -174,31 +165,13 @@ class EventDispatcherTest extends TestCase
return array();
}));
- $io->expects($this->any())
- ->method('isVerbose')
- ->willReturn(1);
-
- $io->expects($this->at(1))
- ->method('writeError')
- ->with($this->equalTo('> root: @group'));
-
- $io->expects($this->at(3))
- ->method('writeError')
- ->with($this->equalTo('> group: echo -n foo'));
-
- $io->expects($this->at(5))
- ->method('writeError')
- ->with($this->equalTo('> group: @subgroup'));
-
- $io->expects($this->at(7))
- ->method('writeError')
- ->with($this->equalTo('> subgroup: echo -n baz'));
-
- $io->expects($this->at(9))
- ->method('writeError')
- ->with($this->equalTo('> group: echo -n bar'));
-
$dispatcher->dispatch('root', new CommandEvent('root', $composer, $io));
+ $expected = '> root: @group'.PHP_EOL.
+ '> group: echo -n foo'.PHP_EOL.
+ '> group: @subgroup'.PHP_EOL.
+ '> subgroup: echo -n baz'.PHP_EOL.
+ '> group: echo -n bar'.PHP_EOL;
+ $this->assertEquals($expected, $io->getOutput());
}
/**
diff --git a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test
index 26861b1c6..1e0b9ff2c 100644
--- a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test
+++ b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test
@@ -24,12 +24,12 @@ Abandoned packages are flagged
--RUN--
install
--EXPECT-OUTPUT--
-Loading composer repositories with package information
-Installing dependencies (including require-dev)
+Loading composer repositories with package information
+Installing dependencies (including require-dev)
Package a/a is abandoned, you should avoid using it. No replacement was suggested.
Package c/c is abandoned, you should avoid using it. Use b/b instead.
-Writing lock file
-Generating autoload files
+Writing lock file
+Generating autoload files
--EXPECT--
Installing a/a (1.0.0)
diff --git a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test
index 19bd8f914..e7c6cd984 100644
--- a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test
+++ b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test
@@ -21,9 +21,9 @@ Broken dependencies should not lead to a replacer being installed which is not m
--RUN--
install
--EXPECT-OUTPUT--
-Loading composer repositories with package information
-Installing dependencies (including require-dev)
-Your requirements could not be resolved to an installable set of packages.
+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 for more details.
+ see for more details.
Read for further common problems.
diff --git a/tests/Composer/Test/Fixtures/installer/install-self-from-root.test b/tests/Composer/Test/Fixtures/installer/install-self-from-root.test
new file mode 100644
index 000000000..82092c77f
--- /dev/null
+++ b/tests/Composer/Test/Fixtures/installer/install-self-from-root.test
@@ -0,0 +1,16 @@
+--TEST--
+Tries to require a package with the same name as the root package
+--COMPOSER--
+{
+ "name": "foo/bar",
+ "require": {
+ "foo/bar": "@dev"
+ }
+}
+--RUN--
+install
+--EXPECT-EXCEPTION--
+InvalidArgumentException
+--EXPECT--
+Root package 'foo/bar' cannot require itself in its composer.json
+Did you accidentally name your root package after an external package?
diff --git a/tests/Composer/Test/Fixtures/installer/suggest-installed.test b/tests/Composer/Test/Fixtures/installer/suggest-installed.test
index 94f6c2016..4929f972e 100644
--- a/tests/Composer/Test/Fixtures/installer/suggest-installed.test
+++ b/tests/Composer/Test/Fixtures/installer/suggest-installed.test
@@ -19,10 +19,10 @@ Suggestions are not displayed for installed packages
--RUN--
install
--EXPECT-OUTPUT--
-Loading composer repositories with package information
-Installing dependencies (including require-dev)
-Writing lock file
-Generating autoload files
+Loading composer repositories with package information
+Installing dependencies (including require-dev)
+Writing lock file
+Generating autoload files
--EXPECT--
Installing a/a (1.0.0)
diff --git a/tests/Composer/Test/Fixtures/installer/suggest-prod.test b/tests/Composer/Test/Fixtures/installer/suggest-prod.test
index 290ccf4bb..c89bb0c20 100644
--- a/tests/Composer/Test/Fixtures/installer/suggest-prod.test
+++ b/tests/Composer/Test/Fixtures/installer/suggest-prod.test
@@ -17,10 +17,10 @@ Suggestions are not displayed in non-dev mode
--RUN--
install --no-dev
--EXPECT-OUTPUT--
-Loading composer repositories with package information
-Installing dependencies
-Writing lock file
-Generating autoload files
+Loading composer repositories with package information
+Installing dependencies
+Writing lock file
+Generating autoload files
--EXPECT--
Installing a/a (1.0.0)
diff --git a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test
index 99d13a720..5d64d2176 100644
--- a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test
+++ b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test
@@ -19,10 +19,10 @@ Suggestions are not displayed for packages if they are replaced
--RUN--
install
--EXPECT-OUTPUT--
-Loading composer repositories with package information
-Installing dependencies (including require-dev)
-Writing lock file
-Generating autoload files
+Loading composer repositories with package information
+Installing dependencies (including require-dev)
+Writing lock file
+Generating autoload files
--EXPECT--
Installing c/c (1.0.0)
diff --git a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test
index d7e026e98..d04b6c8d5 100644
--- a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test
+++ b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test
@@ -17,11 +17,11 @@ Suggestions are displayed
--RUN--
install
--EXPECT-OUTPUT--
-Loading composer repositories with package information
-Installing dependencies (including require-dev)
+Loading composer repositories with package information
+Installing dependencies (including require-dev)
a/a suggests installing b/b (an obscure reason)
-Writing lock file
-Generating autoload files
+Writing lock file
+Generating autoload files
--EXPECT--
Installing a/a (1.0.0)
diff --git a/tests/Composer/Test/IO/ConsoleIOTest.php b/tests/Composer/Test/IO/ConsoleIOTest.php
index a300350b9..ca7d420c9 100644
--- a/tests/Composer/Test/IO/ConsoleIOTest.php
+++ b/tests/Composer/Test/IO/ConsoleIOTest.php
@@ -14,6 +14,7 @@ namespace Composer\Test\IO;
use Composer\IO\ConsoleIO;
use Composer\TestCase;
+use Symfony\Component\Console\Output\OutputInterface;
class ConsoleIOTest extends TestCase
{
@@ -40,6 +41,9 @@ class ConsoleIOTest extends TestCase
{
$inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
$outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+ $outputMock->expects($this->once())
+ ->method('getVerbosity')
+ ->willReturn(OutputInterface::VERBOSITY_NORMAL);
$outputMock->expects($this->once())
->method('write')
->with($this->equalTo('some information about something'), $this->equalTo(false));
@@ -53,6 +57,9 @@ class ConsoleIOTest extends TestCase
{
$inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
$outputMock = $this->getMock('Symfony\Component\Console\Output\ConsoleOutputInterface');
+ $outputMock->expects($this->once())
+ ->method('getVerbosity')
+ ->willReturn(OutputInterface::VERBOSITY_NORMAL);
$outputMock->expects($this->once())
->method('getErrorOutput')
->willReturn($outputMock);
@@ -69,6 +76,9 @@ class ConsoleIOTest extends TestCase
{
$inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
$outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+ $outputMock->expects($this->once())
+ ->method('getVerbosity')
+ ->willReturn(OutputInterface::VERBOSITY_NORMAL);
$outputMock->expects($this->once())
->method('write')
->with(
@@ -95,25 +105,28 @@ class ConsoleIOTest extends TestCase
$inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface');
$outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
- $outputMock->expects($this->at(0))
- ->method('write')
- ->with($this->equalTo('something (strlen = 23)'));
+ $outputMock->expects($this->any())
+ ->method('getVerbosity')
+ ->willReturn(OutputInterface::VERBOSITY_NORMAL);
$outputMock->expects($this->at(1))
->method('write')
- ->with($this->equalTo(str_repeat("\x08", 23)), $this->equalTo(false));
- $outputMock->expects($this->at(2))
- ->method('write')
- ->with($this->equalTo('shorter (12)'), $this->equalTo(false));
+ ->with($this->equalTo('something (strlen = 23)'));
$outputMock->expects($this->at(3))
->method('write')
- ->with($this->equalTo(str_repeat(' ', 11)), $this->equalTo(false));
- $outputMock->expects($this->at(4))
- ->method('write')
- ->with($this->equalTo(str_repeat("\x08", 11)), $this->equalTo(false));
+ ->with($this->equalTo(str_repeat("\x08", 23)), $this->equalTo(false));
$outputMock->expects($this->at(5))
+ ->method('write')
+ ->with($this->equalTo('shorter (12)'), $this->equalTo(false));
+ $outputMock->expects($this->at(7))
+ ->method('write')
+ ->with($this->equalTo(str_repeat(' ', 11)), $this->equalTo(false));
+ $outputMock->expects($this->at(9))
+ ->method('write')
+ ->with($this->equalTo(str_repeat("\x08", 11)), $this->equalTo(false));
+ $outputMock->expects($this->at(11))
->method('write')
->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 (34)'));
diff --git a/tests/Composer/Test/Installer/LibraryInstallerTest.php b/tests/Composer/Test/Installer/LibraryInstallerTest.php
index 6230752e5..72eeb04b1 100644
--- a/tests/Composer/Test/Installer/LibraryInstallerTest.php
+++ b/tests/Composer/Test/Installer/LibraryInstallerTest.php
@@ -22,6 +22,7 @@ class LibraryInstallerTest extends TestCase
{
protected $composer;
protected $config;
+ protected $rootDir;
protected $vendorDir;
protected $binDir;
protected $dm;
@@ -37,10 +38,11 @@ class LibraryInstallerTest extends TestCase
$this->config = new Config();
$this->composer->setConfig($this->config);
- $this->vendorDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'composer-test-vendor';
+ $this->rootDir = $this->getUniqueTmpDirectory();
+ $this->vendorDir = $this->rootDir.DIRECTORY_SEPARATOR.'vendor';
$this->ensureDirectoryExistsAndClear($this->vendorDir);
- $this->binDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'composer-test-bin';
+ $this->binDir = $this->rootDir.DIRECTORY_SEPARATOR.'bin';
$this->ensureDirectoryExistsAndClear($this->binDir);
$this->config->merge(array(
@@ -61,8 +63,7 @@ class LibraryInstallerTest extends TestCase
protected function tearDown()
{
- $this->fs->removeDirectory($this->vendorDir);
- $this->fs->removeDirectory($this->binDir);
+ $this->fs->removeDirectory($this->rootDir);
}
public function testInstallerCreationShouldNotCreateVendorDirectory()
diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php
index 5339b8ff3..eaf6caa03 100644
--- a/tests/Composer/Test/InstallerTest.php
+++ b/tests/Composer/Test/InstallerTest.php
@@ -26,7 +26,10 @@ use Composer\Test\Mock\InstalledFilesystemRepositoryMock;
use Composer\Test\Mock\InstallationManagerMock;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\StreamOutput;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Formatter\OutputFormatter;
use Composer\TestCase;
+use Composer\IO\BufferIO;
class InstallerTest extends TestCase
{
@@ -137,7 +140,7 @@ class InstallerTest extends TestCase
/**
* @dataProvider getIntegrationTests
*/
- public function testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectExitCode)
+ public function testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectResult)
{
if ($condition) {
eval('$res = '.$condition.';');
@@ -146,18 +149,15 @@ class InstallerTest extends TestCase
}
}
- $output = null;
- $io = $this->getMock('Composer\IO\IOInterface');
- $callback = function ($text, $newline) use (&$output) {
- $output .= $text . ($newline ? "\n" : "");
- };
- $io->expects($this->any())
- ->method('write')
- ->will($this->returnCallback($callback));
- $io->expects($this->any())
- ->method('writeError')
- ->will($this->returnCallback($callback));
+ $io = new BufferIO('', OutputInterface::VERBOSITY_NORMAL, new OutputFormatter(false));
+ // Prepare for exceptions
+ if (!is_int($expectResult)) {
+ $normalizedOutput = rtrim(str_replace("\n", PHP_EOL, $expect));
+ $this->setExpectedException($expectResult, $normalizedOutput);
+ }
+
+ // Create Composer mock object according to configuration
$composer = FactoryMock::create($io, $composerConfig);
$jsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock();
@@ -233,8 +233,14 @@ class InstallerTest extends TestCase
$appOutput = fopen('php://memory', 'w+');
$result = $application->run(new StringInput($run), new StreamOutput($appOutput));
fseek($appOutput, 0);
- $this->assertEquals($expectExitCode, $result, $output . stream_get_contents($appOutput));
+ // Shouldn't check output and results if an exception was expected by this point
+ if (!is_int($expectResult)) {
+ return;
+ }
+
+ $output = str_replace("\r", '', $io->getOutput());
+ $this->assertEquals($expectResult, $result, $output . stream_get_contents($appOutput));
if ($expectLock) {
unset($actualLock['hash']);
unset($actualLock['content-hash']);
@@ -266,7 +272,7 @@ class InstallerTest extends TestCase
$installedDev = array();
$lock = array();
$expectLock = array();
- $expectExitCode = 0;
+ $expectResult = 0;
try {
$message = $testData['TEST'];
@@ -303,12 +309,21 @@ class InstallerTest extends TestCase
}
$expectOutput = isset($testData['EXPECT-OUTPUT']) ? $testData['EXPECT-OUTPUT'] : null;
$expect = $testData['EXPECT'];
- $expectExitCode = isset($testData['EXPECT-EXIT-CODE']) ? (int) $testData['EXPECT-EXIT-CODE'] : 0;
+ if (!empty($testData['EXPECT-EXCEPTION'])) {
+ $expectResult = $testData['EXPECT-EXCEPTION'];
+ if (!empty($testData['EXPECT-EXIT-CODE'])) {
+ throw new \LogicException('EXPECT-EXCEPTION and EXPECT-EXIT-CODE are mutually exclusive');
+ }
+ } elseif (!empty($testData['EXPECT-EXIT-CODE'])) {
+ $expectResult = (int) $testData['EXPECT-EXIT-CODE'];
+ } else {
+ $expectResult = 0;
+ }
} catch (\Exception $e) {
die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file)));
}
- $tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectExitCode);
+ $tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectResult);
}
return $tests;
@@ -328,6 +343,7 @@ class InstallerTest extends TestCase
'EXPECT-LOCK' => false,
'EXPECT-OUTPUT' => false,
'EXPECT-EXIT-CODE' => false,
+ 'EXPECT-EXCEPTION' => false,
'EXPECT' => true,
);
diff --git a/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php b/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php
index f395eba6e..cce67c1aa 100644
--- a/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php
+++ b/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php
@@ -13,11 +13,12 @@
namespace Composer\Test\Package\Archiver;
use Composer\Package\Archiver\ArchivableFilesFinder;
+use Composer\TestCase;
use Composer\Util\Filesystem;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\ExecutableFinder;
-class ArchivableFilesFinderTest extends \PHPUnit_Framework_TestCase
+class ArchivableFilesFinderTest extends TestCase
{
protected $sources;
protected $finder;
@@ -29,7 +30,7 @@ class ArchivableFilesFinderTest extends \PHPUnit_Framework_TestCase
$this->fs = $fs;
$this->sources = $fs->normalizePath(
- realpath(sys_get_temp_dir()).'/composer_archiver_test'.uniqid(mt_rand(), true)
+ $this->getUniqueTmpDirectory()
);
$fileTree = array(
diff --git a/tests/Composer/Test/Package/Archiver/ArchiverTest.php b/tests/Composer/Test/Package/Archiver/ArchiverTest.php
index a3c73fa7a..32a6ed749 100644
--- a/tests/Composer/Test/Package/Archiver/ArchiverTest.php
+++ b/tests/Composer/Test/Package/Archiver/ArchiverTest.php
@@ -12,11 +12,12 @@
namespace Composer\Test\Package\Archiver;
+use Composer\TestCase;
use Composer\Util\Filesystem;
use Composer\Util\ProcessExecutor;
use Composer\Package\Package;
-abstract class ArchiverTest extends \PHPUnit_Framework_TestCase
+abstract class ArchiverTest extends TestCase
{
/**
* @var \Composer\Util\Filesystem
@@ -37,8 +38,7 @@ abstract class ArchiverTest extends \PHPUnit_Framework_TestCase
{
$this->filesystem = new Filesystem();
$this->process = new ProcessExecutor();
- $this->testDir = sys_get_temp_dir().'/composer_archiver_test_'.mt_rand();
- $this->filesystem->ensureDirectoryExists($this->testDir);
+ $this->testDir = $this->getUniqueTmpDirectory();
}
public function tearDown()
diff --git a/tests/Composer/Test/Package/Archiver/PharArchiverTest.php b/tests/Composer/Test/Package/Archiver/PharArchiverTest.php
index d6e783c91..16753784d 100644
--- a/tests/Composer/Test/Package/Archiver/PharArchiverTest.php
+++ b/tests/Composer/Test/Package/Archiver/PharArchiverTest.php
@@ -21,14 +21,14 @@ class PharArchiverTest extends ArchiverTest
// Set up repository
$this->setupDummyRepo();
$package = $this->setupPackage();
- $target = sys_get_temp_dir().'/composer_archiver_test.tar';
+ $target = $this->getUniqueTmpDirectory().'/composer_archiver_test.tar';
// Test archive
$archiver = new PharArchiver();
$archiver->archive($package->getSourceUrl(), $target, 'tar', array('foo/bar', 'baz', '!/foo/bar/baz'));
$this->assertFileExists($target);
- unlink($target);
+ $this->filesystem->removeDirectory(dirname($target));
}
public function testZipArchive()
@@ -36,14 +36,14 @@ class PharArchiverTest extends ArchiverTest
// Set up repository
$this->setupDummyRepo();
$package = $this->setupPackage();
- $target = sys_get_temp_dir().'/composer_archiver_test.zip';
+ $target = $this->getUniqueTmpDirectory().'/composer_archiver_test.zip';
// Test archive
$archiver = new PharArchiver();
$archiver->archive($package->getSourceUrl(), $target, 'zip');
$this->assertFileExists($target);
- unlink($target);
+ $this->filesystem->removeDirectory(dirname($target));
}
/**
diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json
index efc552956..574c4402f 100644
--- a/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json
+++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json
@@ -7,6 +7,6 @@
"class": "Installer\\Plugin"
},
"require": {
- "composer-plugin-api": "1.0.0"
+ "composer-plugin-api": "^1.0"
}
}
diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json
index 6947ddd5c..27432acfa 100644
--- a/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json
+++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json
@@ -7,6 +7,6 @@
"class": "Installer\\Plugin2"
},
"require": {
- "composer-plugin-api": "1.0.0"
+ "composer-plugin-api": "^1.0"
}
}
diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json
index 5cb01d019..881eb5cae 100644
--- a/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json
+++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json
@@ -7,6 +7,6 @@
"class": "Installer\\Plugin2"
},
"require": {
- "composer-plugin-api": "1.0.0"
+ "composer-plugin-api": "^1.0"
}
}
diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json
index 982d34c7b..f61cb3fbd 100644
--- a/tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json
+++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json
@@ -10,6 +10,6 @@
]
},
"require": {
- "composer-plugin-api": "1.0.0"
+ "composer-plugin-api": "^1.0"
}
}
diff --git a/tests/Composer/Test/Plugin/Mock/Capability.php b/tests/Composer/Test/Plugin/Mock/Capability.php
new file mode 100644
index 000000000..79635a314
--- /dev/null
+++ b/tests/Composer/Test/Plugin/Mock/Capability.php
@@ -0,0 +1,23 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Plugin\Mock;
+
+class Capability implements \Composer\Plugin\Capability\Capability
+{
+ public $args;
+
+ public function __construct(array $args)
+ {
+ $this->args = $args;
+ }
+}
diff --git a/tests/Composer/Test/Plugin/Mock/CapablePluginInterface.php b/tests/Composer/Test/Plugin/Mock/CapablePluginInterface.php
new file mode 100644
index 000000000..5e8d88c31
--- /dev/null
+++ b/tests/Composer/Test/Plugin/Mock/CapablePluginInterface.php
@@ -0,0 +1,20 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Plugin\Mock;
+
+use Composer\Plugin\Capable;
+use Composer\Plugin\PluginInterface;
+
+interface CapablePluginInterface extends PluginInterface, Capable
+{
+}
diff --git a/tests/Composer/Test/Plugin/PluginInstallerTest.php b/tests/Composer/Test/Plugin/PluginInstallerTest.php
index b449d7e90..50c7178da 100644
--- a/tests/Composer/Test/Plugin/PluginInstallerTest.php
+++ b/tests/Composer/Test/Plugin/PluginInstallerTest.php
@@ -69,7 +69,7 @@ class PluginInstallerTest extends TestCase
{
$loader = new JsonLoader(new ArrayLoader());
$this->packages = array();
- $this->directory = sys_get_temp_dir() . '/' . uniqid();
+ $this->directory = $this->getUniqueTmpDirectory();
for ($i = 1; $i <= 7; $i++) {
$filename = '/Fixtures/plugin-v'.$i.'/composer.json';
mkdir(dirname($this->directory . $filename), 0777, true);
@@ -147,7 +147,7 @@ class PluginInstallerTest extends TestCase
$this->repository
->expects($this->exactly(2))
->method('getPackages')
- ->will($this->returnValue(array()));
+ ->will($this->returnValue(array($this->packages[3])));
$installer = new PluginInstaller($this->io, $this->composer);
$this->pm->loadInstalledPlugins();
@@ -249,24 +249,6 @@ class PluginInstallerTest extends TestCase
$this->pm->loadInstalledPlugins();
}
- public function testExactPluginVersionStyleAreRegisteredCorrectly()
- {
- $pluginsWithFixedAPIVersions = array(
- $this->packages[0],
- $this->packages[1],
- $this->packages[2],
- );
-
- $this->setPluginApiVersionWithPlugins('1.0.0', $pluginsWithFixedAPIVersions);
- $this->assertCount(3, $this->pm->getPlugins());
-
- $this->setPluginApiVersionWithPlugins('1.0.1', $pluginsWithFixedAPIVersions);
- $this->assertCount(0, $this->pm->getPlugins());
-
- $this->setPluginApiVersionWithPlugins('2.0.0-dev', $pluginsWithFixedAPIVersions);
- $this->assertCount(0, $this->pm->getPlugins());
- }
-
public function testStarPluginVersionWorksWithAnyAPIVersion()
{
$starVersionPlugin = array($this->packages[4]);
@@ -314,4 +296,100 @@ class PluginInstallerTest extends TestCase
$this->setPluginApiVersionWithPlugins('5.5.0', $pluginWithApiConstraint);
$this->assertCount(0, $this->pm->getPlugins());
}
+
+ public function testIncapablePluginIsCorrectlyDetected()
+ {
+ $plugin = $this->getMockBuilder('Composer\Plugin\PluginInterface')
+ ->getMock();
+
+ $this->assertNull($this->pm->getPluginCapability($plugin, 'Fake\Ability'));
+ }
+
+ public function testCapabilityImplementsComposerPluginApiClassAndIsConstructedWithArgs()
+ {
+ $capabilityApi = 'Composer\Plugin\Capability\Capability';
+ $capabilityImplementation = 'Composer\Test\Plugin\Mock\Capability';
+
+ $plugin = $this->getMockBuilder('Composer\Test\Plugin\Mock\CapablePluginInterface')
+ ->getMock();
+
+ $plugin->expects($this->once())
+ ->method('getCapabilities')
+ ->will($this->returnCallback(function () use ($capabilityImplementation, $capabilityApi) {
+ return array($capabilityApi => $capabilityImplementation);
+ }));
+
+ $capability = $this->pm->getPluginCapability($plugin, $capabilityApi, array('a' => 1, 'b' => 2));
+
+ $this->assertInstanceOf($capabilityApi, $capability);
+ $this->assertInstanceOf($capabilityImplementation, $capability);
+ $this->assertSame(array('a' => 1, 'b' => 2), $capability->args);
+ }
+
+ public function invalidImplementationClassNames()
+ {
+ return array(
+ array(null),
+ array(""),
+ array(0),
+ array(1000),
+ array(" "),
+ array(array(1)),
+ array(array()),
+ array(new \stdClass()),
+ );
+ }
+
+ public function nonExistingOrInvalidImplementationClassTypes()
+ {
+ return array(
+ array('\stdClass'),
+ array('NonExistentClassLikeMiddleClass'),
+ );
+ }
+
+ /**
+ * @dataProvider invalidImplementationClassNames
+ * @expectedException \UnexpectedValueException
+ */
+ public function testQueryingWithInvalidCapabilityClassNameThrows($invalidImplementationClassNames)
+ {
+ $capabilityApi = 'Composer\Plugin\Capability\Capability';
+
+ $plugin = $this->getMockBuilder('Composer\Test\Plugin\Mock\CapablePluginInterface')
+ ->getMock();
+
+ $plugin->expects($this->once())
+ ->method('getCapabilities')
+ ->will($this->returnCallback(function () use ($invalidImplementationClassNames, $capabilityApi) {
+ return array($capabilityApi => $invalidImplementationClassNames);
+ }));
+
+ $this->pm->getPluginCapability($plugin, $capabilityApi);
+ }
+
+ public function testQueryingNonProvidedCapabilityReturnsNullSafely()
+ {
+ $capabilityApi = 'Composer\Plugin\Capability\MadeUpCapability';
+
+ $plugin = $this->getMockBuilder('Composer\Test\Plugin\Mock\CapablePluginInterface')
+ ->getMock();
+
+ $plugin->expects($this->once())
+ ->method('getCapabilities')
+ ->will($this->returnCallback(function () {
+ return array();
+ }));
+
+ $this->assertNull($this->pm->getPluginCapability($plugin, $capabilityApi));
+ }
+
+ /**
+ * @dataProvider nonExistingOrInvalidImplementationClassTypes
+ * @expectedException \RuntimeException
+ */
+ public function testQueryingWithNonExistingOrWrongCapabilityClassTypesThrows($wrongImplementationClassTypes)
+ {
+ $this->testQueryingWithInvalidCapabilityClassNameThrows($wrongImplementationClassTypes);
+ }
}
diff --git a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php
index 6f8b71d20..cde5eb402 100644
--- a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php
+++ b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php
@@ -42,7 +42,7 @@ class FilesystemRepositoryTest extends TestCase
}
/**
- * @expectedException Composer\Repository\InvalidRepositoryException
+ * @expectedException \Composer\Repository\InvalidRepositoryException
*/
public function testCorruptedRepositoryFile()
{
diff --git a/tests/Composer/Test/Repository/PathRepositoryTest.php b/tests/Composer/Test/Repository/PathRepositoryTest.php
index 47b7ac24f..e76be2bfa 100644
--- a/tests/Composer/Test/Repository/PathRepositoryTest.php
+++ b/tests/Composer/Test/Repository/PathRepositoryTest.php
@@ -101,6 +101,9 @@ class PathRepositoryTest extends TestCase
$package = $packages[0];
$this->assertEquals('test/path-versioned', $package->getName());
- $this->assertEquals(rtrim($relativeUrl, DIRECTORY_SEPARATOR), rtrim($package->getDistUrl(), DIRECTORY_SEPARATOR));
+
+ // Convert platform specific separators back to generic URL slashes
+ $relativeUrl = str_replace(DIRECTORY_SEPARATOR, '/', $relativeUrl);
+ $this->assertEquals(rtrim($relativeUrl, '/'), rtrim($package->getDistUrl(), '/'));
}
}
diff --git a/tests/Composer/Test/Repository/RepositoryManagerTest.php b/tests/Composer/Test/Repository/RepositoryManagerTest.php
index 4293dff66..0a419be6c 100644
--- a/tests/Composer/Test/Repository/RepositoryManagerTest.php
+++ b/tests/Composer/Test/Repository/RepositoryManagerTest.php
@@ -13,22 +13,49 @@
namespace Composer\Repository;
use Composer\TestCase;
+use Composer\Util\Filesystem;
class RepositoryManagerTest extends TestCase
{
+ protected $tmpdir;
+
+ public function setUp()
+ {
+ $this->tmpdir = $this->getUniqueTmpDirectory();
+ }
+
+ public function tearDown()
+ {
+ if (is_dir($this->tmpdir)) {
+ $fs = new Filesystem();
+ $fs->removeDirectory($this->tmpdir);
+ }
+ }
+
/**
* @dataProvider creationCases
*/
- public function testRepoCreation($type, $config, $exception = null)
+ public function testRepoCreation($type, $options, $exception = null)
{
if ($exception) {
$this->setExpectedException($exception);
}
+
$rm = new RepositoryManager(
$this->getMock('Composer\IO\IOInterface'),
- $this->getMock('Composer\Config'),
+ $config = $this->getMock('Composer\Config', array('get')),
$this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock()
);
+
+ $tmpdir = $this->tmpdir;
+ $config
+ ->expects($this->any())
+ ->method('get')
+ ->will($this->returnCallback(function ($arg) use ($tmpdir) {
+ return 'cache-repo-dir' === $arg ? $tmpdir : null;
+ }))
+ ;
+
$rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository');
$rm->setRepositoryClass('vcs', 'Composer\Repository\VcsRepository');
$rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository');
@@ -40,7 +67,7 @@ class RepositoryManagerTest extends TestCase
$rm->setRepositoryClass('artifact', 'Composer\Repository\ArtifactRepository');
$rm->createRepository('composer', array('url' => 'http://example.org'));
- $rm->createRepository($type, $config);
+ $rm->createRepository($type, $options);
}
public function creationCases()
diff --git a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php
index cd40a71f2..ee7ad38fd 100644
--- a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php
+++ b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php
@@ -14,19 +14,22 @@ namespace Composer\Test\Repository\Vcs;
use Composer\Downloader\TransportException;
use Composer\Repository\Vcs\GitHubDriver;
+use Composer\TestCase;
use Composer\Util\Filesystem;
use Composer\Config;
-class GitHubDriverTest extends \PHPUnit_Framework_TestCase
+class GitHubDriverTest extends TestCase
{
+ private $home;
private $config;
public function setUp()
{
+ $this->home = $this->getUniqueTmpDirectory();
$this->config = new Config();
$this->config->merge(array(
'config' => array(
- 'home' => sys_get_temp_dir() . '/composer-test',
+ 'home' => $this->home,
),
));
}
@@ -34,7 +37,7 @@ class GitHubDriverTest extends \PHPUnit_Framework_TestCase
public function tearDown()
{
$fs = new Filesystem;
- $fs->removeDirectory(sys_get_temp_dir() . '/composer-test');
+ $fs->removeDirectory($this->home);
}
public function testPrivateRepository()
diff --git a/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php
index dc08b9aa8..70bb94843 100644
--- a/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php
+++ b/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php
@@ -14,29 +14,42 @@ namespace Composer\Test\Repository\Vcs;
use Composer\Repository\Vcs\GitLabDriver;
use Composer\Config;
+use Composer\TestCase;
+use Composer\Util\Filesystem;
/**
* @author Jérôme Tamarelle
*/
-class GitLabDriverTest extends \PHPUnit_Framework_TestCase
+class GitLabDriverTest extends TestCase
{
+ private $home;
+ private $config;
+ private $io;
+ private $process;
+ private $remoteFilesystem;
+
public function setUp()
{
+ $this->home = $this->getUniqueTmpDirectory();
$this->config = new Config();
$this->config->merge(array(
'config' => array(
- 'home' => sys_get_temp_dir().'/composer-test',
- 'gitlab-domains' => array('mycompany.com/gitlab', 'gitlab.com')
+ 'home' => $this->home,
+ 'gitlab-domains' => array('mycompany.com/gitlab', 'gitlab.com'),
),
));
$this->io = $this->prophesize('Composer\IO\IOInterface');
-
$this->process = $this->prophesize('Composer\Util\ProcessExecutor');
-
$this->remoteFilesystem = $this->prophesize('Composer\Util\RemoteFilesystem');
}
+ public function tearDown()
+ {
+ $fs = new Filesystem();
+ $fs->removeDirectory($this->home);
+ }
+
public function getInitializeUrls()
{
return array(
diff --git a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php
index 59030f506..987751408 100644
--- a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php
+++ b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php
@@ -13,6 +13,7 @@
namespace Composer\Test\Repository\Vcs;
use Composer\Repository\Vcs\PerforceDriver;
+use Composer\TestCase;
use Composer\Util\Filesystem;
use Composer\Config;
use Composer\Util\Perforce;
@@ -20,7 +21,7 @@ use Composer\Util\Perforce;
/**
* @author Matt Whittom
*/
-class PerforceDriverTest extends \PHPUnit_Framework_TestCase
+class PerforceDriverTest extends TestCase
{
protected $config;
protected $io;
@@ -29,6 +30,7 @@ class PerforceDriverTest extends \PHPUnit_Framework_TestCase
protected $testPath;
protected $driver;
protected $repoConfig;
+ protected $perforce;
const TEST_URL = 'TEST_PERFORCE_URL';
const TEST_DEPOT = 'TEST_DEPOT_CONFIG';
@@ -36,7 +38,7 @@ class PerforceDriverTest extends \PHPUnit_Framework_TestCase
protected function setUp()
{
- $this->testPath = sys_get_temp_dir() . '/composer-test';
+ $this->testPath = $this->getUniqueTmpDirectory();
$this->config = $this->getTestConfig($this->testPath);
$this->repoConfig = $this->getTestRepoConfig();
$this->io = $this->getMockIOInterface();
diff --git a/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php b/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php
index 2ef1baa18..881b86ea2 100644
--- a/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php
+++ b/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php
@@ -14,9 +14,32 @@ namespace Composer\Test\Repository\Vcs;
use Composer\Repository\Vcs\SvnDriver;
use Composer\Config;
+use Composer\TestCase;
+use Composer\Util\Filesystem;
+use Composer\Util\Platform;
-class SvnDriverTest extends \PHPUnit_Framework_TestCase
+class SvnDriverTest extends TestCase
{
+ protected $home;
+ protected $config;
+
+ public function setUp()
+ {
+ $this->home = $this->getUniqueTmpDirectory();
+ $this->config = new Config();
+ $this->config->merge(array(
+ 'config' => array(
+ 'home' => $this->home,
+ ),
+ ));
+ }
+
+ public function tearDown()
+ {
+ $fs = new Filesystem();
+ $fs->removeDirectory($this->home);
+ }
+
/**
* @expectedException RuntimeException
*/
@@ -39,23 +62,17 @@ class SvnDriverTest extends \PHPUnit_Framework_TestCase
->method('execute')
->will($this->returnValue(0));
- $config = new Config();
- $config->merge(array(
- 'config' => array(
- 'home' => sys_get_temp_dir() . '/composer-test',
- ),
- ));
$repoConfig = array(
'url' => 'http://till:secret@corp.svn.local/repo',
);
- $svn = new SvnDriver($repoConfig, $console, $config, $process);
+ $svn = new SvnDriver($repoConfig, $console, $this->config, $process);
$svn->initialize();
}
private function getCmd($cmd)
{
- if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+ if (Platform::isWindows()) {
return strtr($cmd, "'", '"');
}
diff --git a/tests/Composer/Test/Repository/VcsRepositoryTest.php b/tests/Composer/Test/Repository/VcsRepositoryTest.php
index eaedc82a9..61e29be37 100644
--- a/tests/Composer/Test/Repository/VcsRepositoryTest.php
+++ b/tests/Composer/Test/Repository/VcsRepositoryTest.php
@@ -12,6 +12,7 @@
namespace Composer\Test\Repository;
+use Composer\TestCase;
use Symfony\Component\Process\ExecutableFinder;
use Composer\Package\Dumper\ArrayDumper;
use Composer\Repository\VcsRepository;
@@ -23,7 +24,7 @@ use Composer\Config;
/**
* @group slow
*/
-class VcsRepositoryTest extends \PHPUnit_Framework_TestCase
+class VcsRepositoryTest extends TestCase
{
private static $composerHome;
private static $gitRepo;
@@ -32,8 +33,8 @@ class VcsRepositoryTest extends \PHPUnit_Framework_TestCase
protected function initialize()
{
$oldCwd = getcwd();
- self::$composerHome = sys_get_temp_dir() . '/composer-home-'.mt_rand().'/';
- self::$gitRepo = sys_get_temp_dir() . '/composer-git-'.mt_rand().'/';
+ self::$composerHome = $this->getUniqueTmpDirectory();
+ self::$gitRepo = $this->getUniqueTmpDirectory();
$locator = new ExecutableFinder();
if (!$locator->find('git')) {
diff --git a/tests/Composer/Test/Util/FilesystemTest.php b/tests/Composer/Test/Util/FilesystemTest.php
index d7c986369..969572036 100644
--- a/tests/Composer/Test/Util/FilesystemTest.php
+++ b/tests/Composer/Test/Util/FilesystemTest.php
@@ -44,8 +44,8 @@ class FilesystemTest extends TestCase
public function setUp()
{
$this->fs = new Filesystem;
- $this->workingDir = sys_get_temp_dir() . '/composer_testdir';
- $this->testFile = sys_get_temp_dir() . '/composer_test_file';
+ $this->workingDir = $this->getUniqueTmpDirectory();
+ $this->testFile = $this->getUniqueTmpDirectory() . '/composer_test_file';
}
public function tearDown()
@@ -54,7 +54,7 @@ class FilesystemTest extends TestCase
$this->fs->removeDirectory($this->workingDir);
}
if (is_file($this->testFile)) {
- $this->fs->remove($this->testFile);
+ $this->fs->removeDirectory(dirname($this->testFile));
}
}
diff --git a/tests/Composer/Test/Util/PlatformTest.php b/tests/Composer/Test/Util/PlatformTest.php
new file mode 100644
index 000000000..3d82fb96f
--- /dev/null
+++ b/tests/Composer/Test/Util/PlatformTest.php
@@ -0,0 +1,29 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Util;
+
+use Composer\Util\Platform;
+
+/**
+ * PlatformTest
+ *
+ * @author Niels Keurentjes
+ */
+class PlatformTest extends \PHPUnit_Framework_TestCase
+{
+ public function testWindows()
+ {
+ // Compare 2 common tests for Windows to the built-in Windows test
+ $this->assertEquals(('\\' === DIRECTORY_SEPARATOR), Platform::isWindows());
+ $this->assertEquals(defined('PHP_WINDOWS_VERSION_MAJOR'), Platform::isWindows());
+ }
+}
diff --git a/tests/Composer/Test/Util/RemoteFilesystemTest.php b/tests/Composer/Test/Util/RemoteFilesystemTest.php
index 6647e6d5c..73861e396 100644
--- a/tests/Composer/Test/Util/RemoteFilesystemTest.php
+++ b/tests/Composer/Test/Util/RemoteFilesystemTest.php
@@ -172,7 +172,7 @@ class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase
{
$io = $this->getMock('Composer\IO\IOInterface');
- $res = $this->callGetOptionsForUrl($io, array('example.org', array('ssl'=>array('cafile'=>'/some/path/file.crt'))), array(), 'http://www.example.org');
+ $res = $this->callGetOptionsForUrl($io, array('example.org', array('ssl' => array('cafile' => '/some/path/file.crt'))), array(), 'http://www.example.org');
$this->assertTrue(isset($res['ssl']['ciphers']));
$this->assertRegExp("|!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK|", $res['ssl']['ciphers']);
diff --git a/tests/Composer/Test/Util/SilencerTest.php b/tests/Composer/Test/Util/SilencerTest.php
new file mode 100644
index 000000000..5201522f8
--- /dev/null
+++ b/tests/Composer/Test/Util/SilencerTest.php
@@ -0,0 +1,58 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Util;
+
+use Composer\Util\Silencer;
+
+/**
+ * SilencerTest
+ *
+ * @author Niels Keurentjes
+ */
+class SilencerTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * Test succeeds when no warnings are emitted externally, and original level is restored.
+ */
+ public function testSilencer()
+ {
+ $before = error_reporting();
+
+ // Check warnings are suppressed correctly
+ Silencer::suppress();
+ @trigger_error('Test', E_USER_WARNING);
+ Silencer::restore();
+
+ // Check all parameters and return values are passed correctly in a silenced call.
+ $result = Silencer::call(function ($a, $b, $c) {
+ @trigger_error('Test', E_USER_WARNING);
+
+ return $a * $b * $c;
+ }, 2, 3, 4);
+ $this->assertEquals(24, $result);
+
+ // Check the error reporting setting was restored correctly
+ $this->assertEquals($before, error_reporting());
+ }
+
+ /**
+ * Test whether exception from silent callbacks are correctly forwarded.
+ */
+ public function testSilencedException()
+ {
+ $verification = microtime();
+ $this->setExpectedException('\RuntimeException', $verification);
+ Silencer::call(function () use ($verification) {
+ throw new \RuntimeException($verification);
+ });
+ }
+}
diff --git a/tests/Composer/Test/Util/SvnTest.php b/tests/Composer/Test/Util/SvnTest.php
index 55a116376..c16b0e6ce 100644
--- a/tests/Composer/Test/Util/SvnTest.php
+++ b/tests/Composer/Test/Util/SvnTest.php
@@ -14,6 +14,7 @@ namespace Composer\Test\Util;
use Composer\Config;
use Composer\IO\NullIO;
+use Composer\Util\Platform;
use Composer\Util\Svn;
class SvnTest extends \PHPUnit_Framework_TestCase
@@ -131,10 +132,6 @@ class SvnTest 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;
}
}
diff --git a/tests/Composer/Test/Util/TlsHelperTest.php b/tests/Composer/Test/Util/TlsHelperTest.php
new file mode 100644
index 000000000..b17c42ba0
--- /dev/null
+++ b/tests/Composer/Test/Util/TlsHelperTest.php
@@ -0,0 +1,76 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Util;
+
+use Composer\Util\TlsHelper;
+
+class TlsHelperTest extends \PHPUnit_Framework_TestCase
+{
+ /** @dataProvider dataCheckCertificateHost */
+ public function testCheckCertificateHost($expectedResult, $hostname, $certNames)
+ {
+ $certificate['subject']['commonName'] = $expectedCn = array_shift($certNames);
+ $certificate['extensions']['subjectAltName'] = $certNames ? 'DNS:'.implode(',DNS:', $certNames) : '';
+
+ $result = TlsHelper::checkCertificateHost($certificate, $hostname, $foundCn);
+
+ if (true === $expectedResult) {
+ $this->assertTrue($result);
+ $this->assertSame($expectedCn, $foundCn);
+ } else {
+ $this->assertFalse($result);
+ $this->assertNull($foundCn);
+ }
+ }
+
+ public function dataCheckCertificateHost()
+ {
+ return array(
+ array(true, 'getcomposer.org', array('getcomposer.org')),
+ array(true, 'getcomposer.org', array('getcomposer.org', 'packagist.org')),
+ array(true, 'getcomposer.org', array('packagist.org', 'getcomposer.org')),
+ array(true, 'foo.getcomposer.org', array('*.getcomposer.org')),
+ array(false, 'xyz.foo.getcomposer.org', array('*.getcomposer.org')),
+ array(true, 'foo.getcomposer.org', array('getcomposer.org', '*.getcomposer.org')),
+ array(true, 'foo.getcomposer.org', array('foo.getcomposer.org', 'foo*.getcomposer.org')),
+ array(true, 'foo1.getcomposer.org', array('foo.getcomposer.org', 'foo*.getcomposer.org')),
+ array(true, 'foo2.getcomposer.org', array('foo.getcomposer.org', 'foo*.getcomposer.org')),
+ array(false, 'foo2.another.getcomposer.org', array('foo.getcomposer.org', 'foo*.getcomposer.org')),
+ array(false, 'test.example.net', array('**.example.net', '**.example.net')),
+ array(false, 'test.example.net', array('t*t.example.net', 't*t.example.net')),
+ array(false, 'xyz.example.org', array('*z.example.org', '*z.example.org')),
+ array(false, 'foo.bar.example.com', array('foo.*.example.com', 'foo.*.example.com')),
+ array(false, 'example.com', array('example.*', 'example.*')),
+ array(true, 'localhost', array('localhost')),
+ array(false, 'localhost', array('*')),
+ array(false, 'localhost', array('local*')),
+ array(false, 'example.net', array('*.net', '*.org', 'ex*.net')),
+ array(true, 'example.net', array('*.net', '*.org', 'example.net')),
+ );
+ }
+
+ public function testGetCertificateNames()
+ {
+ $certificate['subject']['commonName'] = 'example.net';
+ $certificate['extensions']['subjectAltName'] = 'DNS: example.com, IP: 127.0.0.1, DNS: getcomposer.org, Junk: blah, DNS: composer.example.org';
+
+ $names = TlsHelper::getCertificateNames($certificate);
+
+ $this->assertSame('example.net', $names['cn']);
+ $this->assertSame(array(
+ 'example.com',
+ 'getcomposer.org',
+ 'composer.example.org',
+ ), $names['san']);
+ }
+}
diff --git a/tests/Composer/TestCase.php b/tests/Composer/TestCase.php
index 2057c09b8..7186ae556 100644
--- a/tests/Composer/TestCase.php
+++ b/tests/Composer/TestCase.php
@@ -16,6 +16,7 @@ use Composer\Semver\VersionParser;
use Composer\Package\AliasPackage;
use Composer\Semver\Constraint\Constraint;
use Composer\Util\Filesystem;
+use Composer\Util\Silencer;
abstract class TestCase extends \PHPUnit_Framework_TestCase
{
@@ -56,12 +57,30 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
return new AliasPackage($package, $normVersion, $version);
}
- protected function ensureDirectoryExistsAndClear($directory)
+ protected static function getUniqueTmpDirectory()
+ {
+ $attempts = 5;
+ $root = sys_get_temp_dir();
+
+ do {
+ $unique = $root . DIRECTORY_SEPARATOR . uniqid('composer-test-' . rand(1000, 9000));
+
+ if (!file_exists($unique) && Silencer::call('mkdir', $unique, 0777)) {
+ return realpath($unique);
+ }
+ } while (--$attempts);
+
+ throw new \RuntimeException('Failed to create a unique temporary directory.');
+ }
+
+ protected static function ensureDirectoryExistsAndClear($directory)
{
$fs = new Filesystem();
+
if (is_dir($directory)) {
$fs->removeDirectory($directory);
}
+
mkdir($directory, 0777, true);
}
}