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'); + } + if ($config && $config->get('capath')) { + $remoteFilesystemOptions['ssl']['capath'] = $config->get('capath'); } - $remoteFilesystemOptions = array_merge_recursive($remoteFilesystemOptions, $options); + $remoteFilesystemOptions = array_replace_recursive($remoteFilesystemOptions, $options); } try { $remoteFilesystem = new RemoteFilesystem($io, $config, $remoteFilesystemOptions, $disableTls); @@ -612,7 +625,7 @@ class Factory } /** - * @return boolean + * @return bool */ private static function useXdg() { @@ -626,8 +639,8 @@ class Factory } /** - * @return string * @throws \RuntimeException + * @return string */ private static function getUserDir() { 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; + if ($this->disableTls === false && PHP_VERSION_ID < 50600 && !stream_is_local($this->fileUrl)) { + $host = parse_url($this->fileUrl, PHP_URL_HOST); + + if (PHP_VERSION_ID >= 50304) { + // Must manually follow when setting CN_match because this causes all + // redirects to be validated against the same CN_match value. + $userlandFollow = true; } else { - $host = parse_url($this->fileUrl, PHP_URL_HOST); - } + // PHP < 5.3.4 does not support follow_location, for those people + // do some really nasty hard coded transformations. These will + // still breakdown if the site redirects to a domain we don't + // expect. - if ($host === 'github.com' || $host === 'api.github.com') { - $host = '*.github.com'; + if ($host === 'github.com' || $host === 'api.github.com') { + $host = '*.github.com'; + } } $tlsOptions['ssl']['CN_match'] = $host; $tlsOptions['ssl']['SNI_server_name'] = $host; + + $urlAuthority = $this->getUrlAuthority($this->fileUrl); + + if (isset($this->peerCertificateMap[$urlAuthority])) { + // Handle subjectAltName on lesser PHP's. + $certMap = $this->peerCertificateMap[$urlAuthority]; + + $this->io->writeError(sprintf( + 'Using %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); - - $options['ssl']['cafile'] = $targetPath; - } else { - if (is_dir($result)) { - $options['ssl']['capath'] = $result; - } elseif ($result) { - $options['ssl']['cafile'] = $result; - } + + if (preg_match('{^phar://}', $result)) { + $hash = hash_file('sha256', $result); + $targetPath = rtrim(sys_get_temp_dir(), '\\/') . '/composer-cacert-' . $hash . '.pem'; + + if (!file_exists($targetPath) || $hash !== hash_file('sha256', $targetPath)) { + $this->streamCopy($result, $targetPath); + chmod($targetPath, 0666); } + + $defaults['ssl']['cafile'] = $targetPath; + } elseif (is_dir($result)) { + $defaults['ssl']['capath'] = $result; } else { - throw new TransportException('A valid cafile could not be located automatically.'); + $defaults['ssl']['cafile'] = $result; } } + if (isset($defaults['ssl']['cafile']) && (!is_readable($defaults['ssl']['cafile']) || !$this->validateCaFile($defaults['ssl']['cafile']))) { + throw new TransportException('The configured cafile was not valid or could not be read.'); + } + + if (isset($defaults['ssl']['capath']) && (!is_dir($defaults['ssl']['capath']) || !is_readable($defaults['ssl']['capath']))) { + throw new TransportException('The configured capath was not valid or could not be read.'); + } + /** * Disable TLS compression to prevent CRIME attacks where supported. */ if (PHP_VERSION_ID >= 50413) { - $options['ssl']['disable_compression'] = true; + $defaults['ssl']['disable_compression'] = true; } - return $options; + return $defaults; } /** - * This method was adapted from Sslurp. - * https://github.com/EvanDotPro/Sslurp - * - * (c) Evan Coury - * - * 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)) + $outputMock->expects($this->any()) + ->method('getVerbosity') + ->willReturn(OutputInterface::VERBOSITY_NORMAL); + $outputMock->expects($this->at(1)) ->method('write') ->with($this->equalTo('something (strlen = 23)')); - $outputMock->expects($this->at(1)) + $outputMock->expects($this->at(3)) ->method('write') ->with($this->equalTo(str_repeat("\x08", 23)), $this->equalTo(false)); - $outputMock->expects($this->at(2)) + $outputMock->expects($this->at(5)) ->method('write') ->with($this->equalTo('shorter (12)'), $this->equalTo(false)); - $outputMock->expects($this->at(3)) + $outputMock->expects($this->at(7)) ->method('write') ->with($this->equalTo(str_repeat(' ', 11)), $this->equalTo(false)); - $outputMock->expects($this->at(4)) + $outputMock->expects($this->at(9)) ->method('write') ->with($this->equalTo(str_repeat("\x08", 11)), $this->equalTo(false)); - $outputMock->expects($this->at(5)) + $outputMock->expects($this->at(11)) ->method('write') ->with($this->equalTo(str_repeat("\x08", 12)), $this->equalTo(false)); - $outputMock->expects($this->at(6)) + $outputMock->expects($this->at(13)) ->method('write') ->with($this->equalTo('something longer than initial (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); } }