diff --git a/composer.lock b/composer.lock index dec364ac4..be13e4253 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "composer/ca-bundle", - "version": "1.2.4", + "version": "1.2.6", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "10bb96592168a0f8e8f6dcde3532d9fa50b0b527" + "reference": "47fe531de31fca4a1b997f87308e7d7804348f7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/10bb96592168a0f8e8f6dcde3532d9fa50b0b527", - "reference": "10bb96592168a0f8e8f6dcde3532d9fa50b0b527", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/47fe531de31fca4a1b997f87308e7d7804348f7e", + "reference": "47fe531de31fca4a1b997f87308e7d7804348f7e", "shasum": "" }, "require": { @@ -28,7 +28,7 @@ "require-dev": { "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8", "psr/log": "^1.0", - "symfony/process": "^2.5 || ^3.0 || ^4.0" + "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0" }, "type": "library", "extra": { @@ -46,28 +46,34 @@ "MIT" ], "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", - "time": "2019-08-30T08:44:50+00:00" + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "time": "2020-01-13T10:02:55+00:00" }, { "name": "composer/semver", - "version": "1.5.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e" + "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/46d9139568ccb8d9e7cdd4539cab7347568a5e2e", - "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e", + "url": "https://api.github.com/repos/composer/semver/zipball/c6bea70230ef4dd483e6bbcab6005f682ed3a8de", + "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.5 || ^5.0.5", - "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + "phpunit/phpunit": "^4.5 || ^5.0.5" }, "type": "library", "extra": { @@ -85,7 +91,13 @@ "MIT" ], "description": "Semver library that offers utilities, version constraint parsing and validation.", - "time": "2019-03-19T17:25:45+00:00" + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "time": "2020-01-13T12:06:48+00:00" }, { "name": "composer/spdx-licenses", @@ -387,16 +399,16 @@ }, { "name": "seld/phar-utils", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/Seldaek/phar-utils.git", - "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a" + "reference": "84715761c35808076b00908a20317a3a8a67d17e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/7009b5139491975ef6486545a39f3e6dad5ac30a", - "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/84715761c35808076b00908a20317a3a8a67d17e", + "reference": "84715761c35808076b00908a20317a3a8a67d17e", "shasum": "" }, "require": { @@ -418,7 +430,10 @@ "MIT" ], "description": "PHAR file format utilities, for when PHP phars you up", - "time": "2015-10-13T18:44:15+00:00" + "keywords": [ + "phra" + ], + "time": "2020-01-13T10:41:09+00:00" }, { "name": "symfony/console", @@ -486,7 +501,7 @@ }, { "name": "symfony/debug", - "version": "v2.8.50", + "version": "v2.8.52", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", @@ -651,16 +666,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.12.0", + "version": "v1.13.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "550ebaac289296ce228a706d0867afc34687e3f4" + "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4", - "reference": "550ebaac289296ce228a706d0867afc34687e3f4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", + "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", "shasum": "" }, "require": { @@ -672,7 +687,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.12-dev" + "dev-master": "1.13-dev" } }, "autoload": { @@ -689,20 +704,26 @@ ], "description": "Symfony polyfill for ctype functions", "homepage": "https://symfony.com", - "time": "2019-08-06T08:03:45+00:00" + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2019-11-27T13:56:44+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.12.0", + "version": "v1.13.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17" + "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17", - "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7b4aab9743c30be783b73de055d24a39cf4b954f", + "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f", "shasum": "" }, "require": { @@ -714,7 +735,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.12-dev" + "dev-master": "1.13-dev" } }, "autoload": { @@ -731,7 +752,14 @@ ], "description": "Symfony polyfill for the Mbstring extension", "homepage": "https://symfony.com", - "time": "2019-08-06T08:03:45+00:00" + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2019-11-27T14:18:11+00:00" }, { "name": "symfony/process", diff --git a/doc/01-basic-usage.md b/doc/01-basic-usage.md index cc5ac5459..e1faa6318 100644 --- a/doc/01-basic-usage.md +++ b/doc/01-basic-usage.md @@ -241,14 +241,14 @@ be in your project root, on the same level as `vendor` directory is. An example filename would be `src/Foo.php` containing an `Acme\Foo` class. After adding the [`autoload`](04-schema.md#autoload) field, you have to re-run -this command : +this command : ```sh php composer.phar dump-autoload ``` This command will re-generate the `vendor/autoload.php` file. -See the [`dump-autoload`](03-cli.md#dump-autoload) section for more informations. +See the [`dump-autoload`](03-cli.md#dump-autoload) section for more information. Including that file will also return the autoloader instance, so you can store the return value of the include call in a variable and add more namespaces. diff --git a/doc/articles/scripts.md b/doc/articles/scripts.md index 1185bd689..751d3c492 100644 --- a/doc/articles/scripts.md +++ b/doc/articles/scripts.md @@ -342,6 +342,21 @@ JSON array of commands. You can also call a shell/bash script, which will have the path to the PHP executable available in it as a `PHP_BINARY` env var. +## Setting environment variables + +To set an environment variable in a cross-platform way, you can use `@putenv`: + +```json +{ + "scripts": { + "install-phpstan": [ + "@putenv COMPOSER=phpstan-composer.json", + "composer install --prefer-dist" + ] + } +} +``` + ## Custom descriptions. You can set custom script descriptions with the following in your `composer.json`: diff --git a/res/composer-schema.json b/res/composer-schema.json index bd04c5d5f..c83109151 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -41,7 +41,7 @@ "version": { "type": "string", "description": "Package version, see https://getcomposer.org/doc/04-schema.md#version for more info on valid schemes.", - "pattern": "^v?\\d+(((\\.\\d+)?\\.\\d+)?\\.\\d+)?" + "pattern": "^v?\\d+(((\\.\\d+)?\\.\\d+)?\\.\\d+)?|^dev-" }, "time": { "type": "string", diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index 3bcc665dc..d953809a7 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -575,6 +575,13 @@ EOT $warnings['xdebug_loaded'] = true; } + if (defined('PHP_WINDOWS_VERSION_BUILD') + && (version_compare(PHP_VERSION, '7.2.23', '<') + || (version_compare(PHP_VERSION, '7.3.0', '>=') + && version_compare(PHP_VERSION, '7.3.10', '<')))) { + $warnings['onedrive'] = PHP_VERSION; + } + if (!empty($errors)) { foreach ($errors as $error => $current) { switch ($error) { @@ -684,6 +691,11 @@ EOT $text .= " xdebug.profiler_enabled = 0"; $displayIniMessage = true; break; + + case 'onedrive': + $text = "The Windows OneDrive folder is not supported on PHP versions below 7.2.23 and 7.3.10.".PHP_EOL; + $text .= "Upgrade your PHP ({$current}) to use this location with Composer.".PHP_EOL; + break; } $out($text, 'comment'); } diff --git a/src/Composer/Downloader/SvnDownloader.php b/src/Composer/Downloader/SvnDownloader.php index 35f01eb68..47263c28a 100644 --- a/src/Composer/Downloader/SvnDownloader.php +++ b/src/Composer/Downloader/SvnDownloader.php @@ -138,11 +138,11 @@ class SvnDownloader extends VcsDownloader $this->io->writeError(sprintf(' The package has modified file%s:', $countChanges === 1 ? '' : 's')); $this->io->writeError(array_slice($changes, 0, 10)); if ($countChanges > 10) { - $remaingChanges = $countChanges - 10; + $remainingChanges = $countChanges - 10; $this->io->writeError( sprintf( - ' '.$remaingChanges.' more file%s modified, choose "v" to view the full list', - $remaingChanges === 1 ? '' : 's' + ' '.$remainingChanges.' more file%s modified, choose "v" to view the full list', + $remainingChanges === 1 ? '' : 's' ) ); } diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index f4e54611b..1a5b5b5e8 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -246,7 +246,11 @@ class EventDispatcher } } - if (substr($exec, 0, 5) === '@php ') { + if (substr($exec, 0, 8) === '@putenv ') { + putenv(substr($exec, 8)); + + continue; + } elseif (substr($exec, 0, 5) === '@php ') { $exec = $this->getPhpExecCommand() . ' ' . substr($exec, 5); } else { $finder = new PhpExecutableFinder(); @@ -492,7 +496,7 @@ class EventDispatcher */ protected function isComposerScript($callable) { - return '@' === substr($callable, 0, 1) && '@php ' !== substr($callable, 0, 5); + return '@' === substr($callable, 0, 1) && '@php ' !== substr($callable, 0, 5) && '@putenv ' !== substr($callable, 0, 8); } /** diff --git a/src/Composer/IO/BaseIO.php b/src/Composer/IO/BaseIO.php index 09d9d1663..c62412ea6 100644 --- a/src/Composer/IO/BaseIO.php +++ b/src/Composer/IO/BaseIO.php @@ -64,6 +64,22 @@ abstract class BaseIO implements IOInterface $this->authentications[$repositoryName] = array('username' => $username, 'password' => $password); } + /** + * {@inheritDoc} + */ + public function writeRaw($messages, $newline = true, $verbosity = self::NORMAL) + { + $this->write($messages, $newline, $verbosity); + } + + /** + * {@inheritDoc} + */ + public function writeErrorRaw($messages, $newline = true, $verbosity = self::NORMAL) + { + $this->writeError($messages, $newline, $verbosity); + } + /** * Check for overwrite and set the authentication information for the repository. * diff --git a/src/Composer/IO/ConsoleIO.php b/src/Composer/IO/ConsoleIO.php index 8b29177d5..925a528be 100644 --- a/src/Composer/IO/ConsoleIO.php +++ b/src/Composer/IO/ConsoleIO.php @@ -129,13 +129,29 @@ class ConsoleIO extends BaseIO $this->doWrite($messages, $newline, true, $verbosity); } + /** + * {@inheritDoc} + */ + public function writeRaw($messages, $newline = true, $verbosity = self::NORMAL) + { + $this->doWrite($messages, $newline, false, $verbosity, true); + } + + /** + * {@inheritDoc} + */ + public function writeErrorRaw($messages, $newline = true, $verbosity = self::NORMAL) + { + $this->doWrite($messages, $newline, true, $verbosity, true); + } + /** * @param array|string $messages * @param bool $newline * @param bool $stderr * @param int $verbosity */ - private function doWrite($messages, $newline, $stderr, $verbosity) + private function doWrite($messages, $newline, $stderr, $verbosity, $raw = false) { $sfVerbosity = $this->verbosityMap[$verbosity]; if ($sfVerbosity > $this->output->getVerbosity()) { @@ -149,6 +165,14 @@ class ConsoleIO extends BaseIO $sfVerbosity = OutputInterface::OUTPUT_NORMAL; } + if ($raw) { + if ($sfVerbosity === OutputInterface::OUTPUT_NORMAL) { + $sfVerbosity = OutputInterface::OUTPUT_RAW; + } else { + $sfVerbosity |= OutputInterface::OUTPUT_RAW; + } + } + if (null !== $this->startTime) { $memoryUsage = memory_get_usage() / 1024 / 1024; $timeSpent = microtime(true) - $this->startTime; diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index 420f866f0..25b1b21b0 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -49,6 +49,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt /** @var VersionCacheInterface */ private $versionCache; private $emptyReferences = array(); + private $versionTransportExceptions = array(); public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $dispatcher = null, array $drivers = null, VersionCacheInterface $versionCache = null) { @@ -131,6 +132,11 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt return $this->emptyReferences; } + public function getVersionTransportExceptions() + { + return $this->versionTransportExceptions; + } + protected function initialize() { parent::initialize(); @@ -232,11 +238,14 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt $this->addPackage($this->loader->load($this->preProcess($driver, $data, $identifier))); } catch (\Exception $e) { - if ($e instanceof TransportException && $e->getCode() === 404) { - $this->emptyReferences[] = $identifier; + if ($e instanceof TransportException) { + $this->versionTransportExceptions['tags'][$tag] = $e; + if ($e->getCode() === 404) { + $this->emptyReferences[] = $identifier; + } } if ($isVeryVerbose) { - $this->io->writeError('Skipped tag '.$tag.', '.($e instanceof TransportException ? 'no composer file was found' : $e->getMessage()).''); + $this->io->writeError('Skipped tag '.$tag.', '.($e instanceof TransportException ? 'no composer file was found (' . $e->getCode() . ' HTTP status code)' : $e->getMessage()).''); } continue; } @@ -312,11 +321,12 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt } $this->addPackage($package); } catch (TransportException $e) { + $this->versionTransportExceptions['branches'][$branch] = $e; if ($e->getCode() === 404) { $this->emptyReferences[] = $identifier; } if ($isVeryVerbose) { - $this->io->writeError('Skipped branch '.$branch.', no composer file was found'); + $this->io->writeError('Skipped branch '.$branch.', no composer file was found (' . $e->getCode() . ' HTTP status code)'); } continue; } catch (\Exception $e) { diff --git a/src/Composer/Util/Hg.php b/src/Composer/Util/Hg.php index 8cf6241a6..3681ad5c7 100644 --- a/src/Composer/Util/Hg.php +++ b/src/Composer/Util/Hg.php @@ -53,7 +53,7 @@ class Hg return; } - // Try with the authentication informations available + // Try with the authentication information available if (preg_match('{^(https?)://((.+)(?:\:(.+))?@)?([^/]+)(/.*)?}mi', $url, $match) && $this->io->hasAuthentication($match[5])) { $auth = $this->io->getAuthentication($match[5]); $authenticatedUrl = $match[1] . '://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[5] . (!empty($match[6]) ? $match[6] : null); diff --git a/src/Composer/Util/NoProxyPattern.php b/src/Composer/Util/NoProxyPattern.php index a6cb112be..adf60f0f7 100644 --- a/src/Composer/Util/NoProxyPattern.php +++ b/src/Composer/Util/NoProxyPattern.php @@ -12,34 +12,76 @@ namespace Composer\Util; +use stdClass; + /** - * Tests URLs against no_proxy patterns. + * Tests URLs against NO_PROXY patterns */ class NoProxyPattern { /** * @var string[] */ + protected $hostNames = array(); + + /** + * @var object[] + */ protected $rules = array(); /** - * @param string $pattern no_proxy pattern + * @var bool + */ + protected $noproxy; + + /** + * @param string $pattern NO_PROXY pattern */ public function __construct($pattern) { - $this->rules = preg_split("/[\s,]+/", $pattern); + $this->hostNames = preg_split('{[\s,]+}', $pattern, null, PREG_SPLIT_NO_EMPTY); + $this->noproxy = empty($this->hostNames) || '*' === $this->hostNames[0]; } /** - * Test a URL against the stored pattern. + * Returns true if a URL matches the NO_PROXY pattern * * @param string $url * - * @return bool true if the URL matches one of the rules. + * @return bool */ public function test($url) { - $host = parse_url($url, PHP_URL_HOST); + if ($this->noproxy) { + return true; + } + + if (!$urlData = $this->getUrlData($url)) { + return false; + } + + foreach ($this->hostNames as $index => $hostName) { + if ($this->match($index, $hostName, $urlData)) { + return true; + } + } + + return false; + } + + /** + * Returns false is the url cannot be parsed, otherwise a data object + * + * @param string $url + * + * @return bool|stdclass + */ + protected function getUrlData($url) + { + if (!$host = parse_url($url, PHP_URL_HOST)) { + return false; + } + $port = parse_url($url, PHP_URL_PORT); if (empty($port)) { @@ -53,95 +95,341 @@ class NoProxyPattern } } - foreach ($this->rules as $rule) { - if ($rule == '*') { - return true; - } + $hostName = $host . ($port ? ':' . $port : ''); + list($host, $port, $err) = $this->splitHostPort($hostName); - $match = false; - - list($ruleHost) = explode(':', $rule); - list($base) = explode('/', $ruleHost); - - if (filter_var($base, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { - // ip or cidr match - - if (!isset($ip)) { - $ip = gethostbyname($host); - } - - if (strpos($ruleHost, '/') === false) { - $match = $ip === $ruleHost; - } else { - // gethostbyname() failed to resolve $host to an ip, so we assume - // it must be proxied to let the proxy's DNS resolve it - if ($ip === $host) { - $match = false; - } else { - // match resolved IP against the rule - $match = self::inCIDRBlock($ruleHost, $ip); - } - } - } else { - // match end of domain - - $haystack = '.' . trim($host, '.') . '.'; - $needle = '.'. trim($ruleHost, '.') .'.'; - $match = stripos(strrev($haystack), strrev($needle)) === 0; - } - - // final port check - if ($match && strpos($rule, ':') !== false) { - list(, $rulePort) = explode(':', $rule); - if (!empty($rulePort) && $port != $rulePort) { - $match = false; - } - } - - if ($match) { - return true; - } + if ($err || !$this->ipCheckData($host, $ipdata)) { + return false; } - return false; + return $this->makeData($host, $port, $ipdata); } /** - * Check an IP address against a CIDR + * Returns true if the url is matched by a rule * - * http://framework.zend.com/svn/framework/extras/incubator/library/ZendX/Whois/Adapter/Cidr.php - * - * @param string $cidr IPv4 block in CIDR notation - * @param string $ip IPv4 address + * @param int $index + * @param string $hostName + * @param string $url * * @return bool */ - private static function inCIDRBlock($cidr, $ip) + protected function match($index, $hostName, $url) { - // Get the base and the bits from the CIDR - list($base, $bits) = explode('/', $cidr); + if (!$rule = $this->getRule($index, $hostName)) { + // Data must have been misformatted + return false; + } - // Now split it up into it's classes - list($a, $b, $c, $d) = explode('.', $base); + if ($rule->ipdata) { + // Match ipdata first + if (!$url->ipdata) { + return false; + } - // Now do some bit shifting/switching to convert to ints - $i = ($a << 24) + ($b << 16) + ($c << 8) + $d; - $mask = $bits == 0 ? 0 : (~0 << (32 - $bits)); + if ($rule->ipdata->netmask) { + return $this->matchRange($rule->ipdata, $url->ipdata); + } - // Here's our lowest int - $low = $i & $mask; + $match = $rule->ipdata->ip === $url->ipdata->ip; + } else { + // Match host and port + $haystack = substr($url->name, - strlen($rule->name)); + $match = stripos($haystack, $rule->name) === 0; + } - // Here's our highest int - $high = $i | (~$mask & 0xFFFFFFFF); + if ($match && $rule->port) { + $match = $rule->port === $url->port; + } - // Now split the ip we're checking against up into classes - list($a, $b, $c, $d) = explode('.', $ip); + return $match; + } - // Now convert the ip we're checking against to an int - $check = ($a << 24) + ($b << 16) + ($c << 8) + $d; + /** + * Returns true if the target ip is in the network range + * + * @param stdClass $network + * @param stdClass $target + * + * @return bool + */ + protected function matchRange(stdClass $network, stdClass $target) + { + $net = unpack('C*', $network->ip); + $mask = unpack('C*', $network->netmask); + $ip = unpack('C*', $target->ip); - // If the ip is within the range, including highest/lowest values, - // then it's within the CIDR range - return $check >= $low && $check <= $high; + for ($i = 1; $i < 17; ++$i) { + if (($net[$i] & $mask[$i]) !== ($ip[$i] & $mask[$i])) { + return false; + } + } + + return true; + } + + /** + * Finds or creates rule data for a hostname + * + * @param int $index + * @param string $hostName + * + * @return {null|stdClass} Null if the hostname is invalid + */ + private function getRule($index, $hostName) + { + if (array_key_exists($index, $this->rules)) { + return $this->rules[$index]; + } + + $this->rules[$index] = null; + list($host, $port, $err) = $this->splitHostPort($hostName); + + if ($err || !$this->ipCheckData($host, $ipdata, true)) { + return null; + } + + $this->rules[$index] = $this->makeData($host, $port, $ipdata); + + return $this->rules[$index]; + } + + /** + * Creates an object containing IP data if the host is an IP address + * + * @param string $host + * @param null|stdclass $ipdata Set by method if IP address found + * @param bool $allowPrefix Whether a CIDR prefix-length is expected + * + * @return bool False if the host contains invalid data + */ + private function ipCheckData($host, &$ipdata, $allowPrefix = false) + { + $ipdata = null; + $netmask = null; + $prefix = null; + $modified = false; + + // Check for a CIDR prefix-length + if (strpos($host, '/') !== false) { + list($host, $prefix) = explode('/', $host); + + if (!$allowPrefix || !$this->validateInt($prefix, 0, 128)) { + return false; + } + $prefix = (int) $prefix; + $modified = true; + } + + // See if this is an ip address + if (!filter_var($host, FILTER_VALIDATE_IP)) { + return !$modified; + } + + list($ip, $size) = $this->ipGetAddr($host); + + if ($prefix !== null) { + // Check for a valid prefix + if ($prefix > $size * 8) { + return false; + } + + list($ip, $netmask) = $this->ipGetNetwork($ip, $size, $prefix); + } + + $ipdata = $this->makeIpData($ip, $size, $netmask); + + return true; + } + + /** + * Returns an array of the IP in_addr and its byte size + * + * IPv4 addresses are always mapped to IPv6, which simplifies handling + * and comparison. + * + * @param string $host + * + * @return mixed[] in_addr, size + */ + private function ipGetAddr($host) + { + $ip = inet_pton($host); + $size = strlen($ip); + $mapped = $this->ipMapTo6($ip, $size); + + return array($mapped, $size); + } + + /** + * Returns the binary network mask mapped to IPv6 + * + * @param string $prefix CIDR prefix-length + * @param int $size Byte size of in_addr + * + * @return string + */ + private function ipGetMask($prefix, $size) + { + $mask = ''; + + if ($ones = floor($prefix / 8)) { + $mask = str_repeat(chr(255), $ones); + } + + if ($remainder = $prefix % 8) { + $mask .= chr(0xff ^ (0xff >> $remainder)); + } + + $mask = str_pad($mask, $size, chr(0)); + + return $this->ipMapTo6($mask, $size); + } + + /** + * Calculates and returns the network and mask + * + * @param string $rangeIp IP in_addr + * @param int $size Byte size of in_addr + * @param string $prefix CIDR prefix-length + * + * @return string[] network in_addr, binary mask + */ + private function ipGetNetwork($rangeIp, $size, $prefix) + { + $netmask = $this->ipGetMask($prefix, $size); + + // Get the network from the address and mask + $mask = unpack('C*', $netmask); + $ip = unpack('C*', $rangeIp); + $net = ''; + + for ($i = 1; $i < 17; ++$i) { + $net .= chr($ip[$i] & $mask[$i]); + } + + return array($net, $netmask); + } + + /** + * Maps an IPv4 address to IPv6 + * + * @param string $binary in_addr + * @param int $size Byte size of in_addr + * + * @return string Mapped or existing in_addr + */ + private function ipMapTo6($binary, $size) + { + if ($size === 4) { + $prefix = str_repeat(chr(0), 10) . str_repeat(chr(255), 2); + $binary = $prefix . $binary; + } + + return $binary; + } + + /** + * Creates a rule data object + * + * @param string $host + * @param int $port + * @param null|stdclass $ipdata + * + * @return stdclass + */ + private function makeData($host, $port, $ipdata) + { + return (object) array( + 'host' => $host, + 'name' => '.' . ltrim($host, '.'), + 'port' => $port, + 'ipdata' => $ipdata, + ); + } + + /** + * Creates an ip data object + * + * @param string $ip in_addr + * @param int $size Byte size of in_addr + * @param null|string $netmask Network mask + * + * @return stdclass + */ + private function makeIpData($ip, $size, $netmask) + { + return (object) array( + 'ip' => $ip, + 'size' => $size, + 'netmask' => $netmask, + ); + } + + /** + * Splits the hostname into host and port components + * + * @param string $hostName + * + * @return mixed[] host, port, if there was error + */ + private function splitHostPort($hostName) + { + // host, port, err + $error = array('', '', true); + $port = 0; + $ip6 = ''; + + // Check for square-bracket notation + if ($hostName[0] === '[') { + $index = strpos($hostName, ']'); + + // The smallest ip6 address is :: + if (false === $index || $index < 3) { + return $error; + } + + $ip6 = substr($hostName, 1, $index - 1); + $hostName = substr($hostName, $index + 1); + + if (strpbrk($hostName, '[]') !== false + || substr_count($hostName, ':') > 1) { + return $error; + } + } + + if (substr_count($hostName, ':') === 1) { + $index = strpos($hostName, ':'); + $port = substr($hostName, $index + 1); + $hostName = substr($hostName, 0, $index); + + if (!$this->validateInt($port, 1, 65535)) { + return $error; + } + + $port = (int) $port; + } + + $host = $ip6 . $hostName; + + return array($host, $port, false); + } + + /** + * Wrapper around filter_var FILTER_VALIDATE_INT + * + * @param string $int + * @param int $min + * @param int $max + */ + private function validateInt($int, $min, $max) + { + $options = array( + 'options' => array( + 'min_range' => $min, + 'max_range' => $max) + ); + + return false !== filter_var($int, FILTER_VALIDATE_INT, $options); } } diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index 00b2e7547..83f19cf2d 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -112,10 +112,18 @@ class ProcessExecutor return; } - if (Process::ERR === $type) { - $this->io->writeError($buffer, false); + if (method_exists($this->io, 'writeRaw')) { + if (Process::ERR === $type) { + $this->io->writeErrorRaw($buffer, false); + } else { + $this->io->writeRaw($buffer, false); + } } else { - $this->io->write($buffer, false); + if (Process::ERR === $type) { + $this->io->writeError($buffer, false); + } else { + $this->io->write($buffer, false); + } } } diff --git a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index a41d745ff..804008259 100644 --- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -250,6 +250,43 @@ class EventDispatcherTest extends TestCase $this->assertEquals($expected, $io->getOutput()); } + public function testDispatcherCanPutEnv() + { + $process = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); + $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') + ->setConstructorArgs(array( + $this->createComposerInstance(), + $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE), + $process, + )) + ->setMethods(array( + 'getListeners', + )) + ->getMock(); + + $listeners = array( + '@putenv ABC=123', + 'Composer\\Test\\EventDispatcher\\EventDispatcherTest::getTestEnv', + ); + + $dispatcher->expects($this->atLeastOnce()) + ->method('getListeners') + ->will($this->returnValue($listeners)); + + $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false); + + $expected = '> post-install-cmd: @putenv ABC=123'.PHP_EOL. + '> post-install-cmd: Composer\Test\EventDispatcher\EventDispatcherTest::getTestEnv'.PHP_EOL; + $this->assertEquals($expected, $io->getOutput()); + } + + static public function getTestEnv() { + $val = getenv('ABC'); + if ($val !== '123') { + throw new \Exception('getenv() did not return the expected value. expected 123 got '. var_export($val, true)); + } + } + public function testDispatcherCanExecuteComposerScriptGroups() { $process = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); diff --git a/tests/Composer/Test/Util/NoProxyPatternTest.php b/tests/Composer/Test/Util/NoProxyPatternTest.php new file mode 100644 index 000000000..a3c01546c --- /dev/null +++ b/tests/Composer/Test/Util/NoProxyPatternTest.php @@ -0,0 +1,143 @@ + + * 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\NoProxyPattern; +use PHPUnit\Framework\TestCase; + +class NoProxyPatternTest extends TestCase +{ + /** + * @dataProvider dataHostName + */ + public function testHostName($noproxy, $url, $expected) + { + $matcher = new NoProxyPattern($noproxy); + $url = $this->getUrl($url); + $this->assertEquals($expected, $matcher->test($url)); + } + + public function dataHostName() + { + $noproxy = 'foobar.com, .barbaz.net'; + + // noproxy, url, expected + return array( + 'match as foobar.com' => array($noproxy, 'foobar.com', true), + 'match foobar.com' => array($noproxy, 'www.foobar.com', true), + 'no match foobar.com' => array($noproxy, 'foofoobar.com', false), + 'match .barbaz.net 1' => array($noproxy, 'barbaz.net', true), + 'match .barbaz.net 2' => array($noproxy, 'www.barbaz.net', true), + 'no match .barbaz.net' => array($noproxy, 'barbarbaz.net', false), + 'no match wrong domain' => array($noproxy, 'barbaz.com', false), + 'no match FQDN' => array($noproxy, 'foobar.com.', false), + ); + } + + /** + * @dataProvider dataIpAddress + */ + public function testIpAddress($noproxy, $url, $expected) + { + $matcher = new NoProxyPattern($noproxy); + $url = $this->getUrl($url); + $this->assertEquals($expected, $matcher->test($url)); + } + + public function dataIpAddress() + { + $noproxy = '192.168.1.1, 2001:db8::52:0:1'; + + // noproxy, url, expected + return array( + 'match exact IPv4' => array($noproxy, '192.168.1.1', true), + 'no match IPv4' => array($noproxy, '192.168.1.4', false), + 'match exact IPv6' => array($noproxy, '[2001:db8:0:0:0:52:0:1]', true), + 'no match IPv6' => array($noproxy, '[2001:db8:0:0:0:52:0:2]', false), + 'match mapped IPv4' => array($noproxy, '[::FFFF:C0A8:0101]', true), + 'no match mapped IPv4' => array($noproxy, '[::FFFF:C0A8:0104]', false), + ); + } + + /** + * @dataProvider dataIpRange + */ + public function testIpRange($noproxy, $url, $expected) + { + $matcher = new NoProxyPattern($noproxy); + $url = $this->getUrl($url); + $this->assertEquals($expected, $matcher->test($url)); + } + + public function dataIpRange() + { + $noproxy = '10.0.0.0/30, 2002:db8:a::45/121'; + + // noproxy, url, expected + return array( + 'match IPv4/CIDR' => array($noproxy, '10.0.0.2', true), + 'no match IPv4/CIDR' => array($noproxy, '10.0.0.4', false), + 'match IPv6/CIDR' => array($noproxy, '[2002:db8:a:0:0:0:0:7f]', true), + 'no match IPv6' => array($noproxy, '[2002:db8:a:0:0:0:0:ff]', false), + 'match mapped IPv4' => array($noproxy, '[::FFFF:0A00:0002]', true), + 'no match mapped IPv4' => array($noproxy, '[::FFFF:0A00:0004]', false), + ); + } + + /** + * @dataProvider dataPort + */ + public function testPort($noproxy, $url, $expected) + { + $matcher = new NoProxyPattern($noproxy); + $url = $this->getUrl($url); + $this->assertEquals($expected, $matcher->test($url)); + } + + public function dataPort() + { + $noproxy = '192.168.1.2:81, 192.168.1.3:80, [2001:db8::52:0:2]:443, [2001:db8::52:0:3]:80'; + + // noproxy, url, expected + return array( + 'match IPv4 port' => array($noproxy, '192.168.1.3', true), + 'no match IPv4 port' => array($noproxy, '192.168.1.2', false), + 'match IPv6 port' => array($noproxy, '[2001:db8::52:0:3]', true), + 'no match IPv6 port' => array($noproxy, '[2001:db8::52:0:2]', false), + ); + } + + /** + * Appends a scheme to the test url if it is missing + * + * @param string $url + */ + private function getUrl($url) + { + if (parse_url($url, PHP_URL_SCHEME)) { + return $url; + } + + $scheme = 'http'; + + if (strpos($url, '[') !== 0 && strrpos($url, ':') !== false) { + list(, $port) = explode(':', $url); + + if ($port === '443') { + $scheme = 'https'; + } + } + + return sprintf('%s://%s', $scheme, $url); + } +} diff --git a/tests/Composer/Test/Util/ProcessExecutorTest.php b/tests/Composer/Test/Util/ProcessExecutorTest.php index db16b8c02..29723b4a5 100644 --- a/tests/Composer/Test/Util/ProcessExecutorTest.php +++ b/tests/Composer/Test/Util/ProcessExecutorTest.php @@ -12,9 +12,14 @@ namespace Composer\Test\Util; +use Composer\IO\ConsoleIO; use Composer\Util\ProcessExecutor; use Composer\Test\TestCase; use Composer\IO\BufferIO; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\BufferedOutput; +use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; class ProcessExecutorTest extends TestCase @@ -99,4 +104,13 @@ class ProcessExecutorTest extends TestCase $this->assertEquals(array('foo', 'bar'), $process->splitLines("foo\r\nbar")); $this->assertEquals(array('foo', 'bar'), $process->splitLines("foo\r\nbar\n")); } + + public function testConsoleIODoesNotFormatSymfonyConsoleStyle() + { + $output = new BufferedOutput(OutputInterface::VERBOSITY_NORMAL, true); + $process = new ProcessExecutor(new ConsoleIO(new ArrayInput([]), $output, new HelperSet([]))); + + $process->execute('echo \'foo\''); + $this->assertSame('foo'.PHP_EOL, $output->fetch()); + } }