From 7de68bebe65a9fa0cc828ac9ed732aacb538c004 Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Wed, 6 Nov 2019 17:14:14 +0000 Subject: [PATCH 01/13] Update xdebug-handler to 1.4.0 --- composer.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.lock b/composer.lock index 0580c0b1b..6be52fd41 100644 --- a/composer.lock +++ b/composer.lock @@ -186,24 +186,24 @@ }, { "name": "composer/xdebug-handler", - "version": "1.3.3", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f" + "reference": "cbe23383749496fe0f373345208b79568e4bc248" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/46867cbf8ca9fb8d60c506895449eb799db1184f", - "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/cbe23383749496fe0f373345208b79568e4bc248", + "reference": "cbe23383749496fe0f373345208b79568e4bc248", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0", + "php": "^5.3.2 || ^7.0 || ^8.0", "psr/log": "^1.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8" }, "type": "library", "autoload": { @@ -221,12 +221,12 @@ "email": "john-stevenson@blueyonder.co.uk" } ], - "description": "Restarts a process without xdebug.", + "description": "Restarts a process without Xdebug.", "keywords": [ "Xdebug", "performance" ], - "time": "2019-05-27T17:52:04+00:00" + "time": "2019-11-06T16:40:04+00:00" }, { "name": "justinrainbow/json-schema", From b6673612e0d27424a2e35d500776e89a10e4f806 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 29 Nov 2019 13:29:32 +0100 Subject: [PATCH 02/13] Allow dev-* versions in schema, refs #8262 --- res/composer-schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/composer-schema.json b/res/composer-schema.json index b4e9d65af..02477573f 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", From 74ba9decdf4b5d34026a3d5452bbfc023196aa27 Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Fri, 29 Nov 2019 16:47:40 +0000 Subject: [PATCH 03/13] Rewrite NoProxyPattern to include IPv6 This includes two breaking changes: - the hostname is not resolved in the case of an IP address. - a hostname with a trailing period (FQDN) is not matched. This brings the basic implementation in line with curl behaviour, with the addition of full IP address and range matching (curl does not differentiate between IP addresses host names). The NO_PROXY environment variable can be set to either a comma-separated list of host names that should not use a proxy, or single asterisk `*` to match all hosts. - Port numbers can be included by prefixing the port with a colon `:`. - IP addresses can be used, but must be enclosed in square brackets `[...]` if they include a port number. - IP address ranges can specified in CIDR notation, separating the IP address and prefix-length with a forward slash `/`. --- src/Composer/Util/NoProxyPattern.php | 444 +++++++++++++++--- .../Composer/Test/Util/NoProxyPatternTest.php | 143 ++++++ 2 files changed, 509 insertions(+), 78 deletions(-) create mode 100644 tests/Composer/Test/Util/NoProxyPatternTest.php 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/tests/Composer/Test/Util/NoProxyPatternTest.php b/tests/Composer/Test/Util/NoProxyPatternTest.php new file mode 100644 index 000000000..05d149e21 --- /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, 192.168.1.2, 192.168.1.3, 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/64'; + + // 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:123]', true), + 'no match IPv6' => array($noproxy, '[2001:db8::52:0:2]', 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); + } +} From 00da9b125d19743a8997ccb26eabe5be9a509ad0 Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Sat, 30 Nov 2019 15:45:54 +0000 Subject: [PATCH 04/13] Tidy and fix tests --- tests/Composer/Test/Util/NoProxyPatternTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Composer/Test/Util/NoProxyPatternTest.php b/tests/Composer/Test/Util/NoProxyPatternTest.php index 05d149e21..a3c01546c 100644 --- a/tests/Composer/Test/Util/NoProxyPatternTest.php +++ b/tests/Composer/Test/Util/NoProxyPatternTest.php @@ -56,7 +56,7 @@ class NoProxyPatternTest extends TestCase public function dataIpAddress() { - $noproxy = '192.168.1.1, 192.168.1.2, 192.168.1.3, 2001:db8::52:0:1'; + $noproxy = '192.168.1.1, 2001:db8::52:0:1'; // noproxy, url, expected return array( @@ -81,14 +81,14 @@ class NoProxyPatternTest extends TestCase public function dataIpRange() { - $noproxy = '10.0.0.0/30, 2002:db8:a::45/64'; + $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:123]', true), - 'no match IPv6' => array($noproxy, '[2001:db8::52:0:2]', 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), ); From dd2cc3e98517471ff9b5a9447785c2698a7e710b Mon Sep 17 00:00:00 2001 From: Stephan Vock Date: Tue, 17 Dec 2019 10:24:53 +0000 Subject: [PATCH 05/13] VcsRepository: make transport exceptions during initialize run accessible * also display the http status code in composer failed to load a composer.json file --- src/Composer/Repository/VcsRepository.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index 8d8bcbdca..18785c4e7 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -45,6 +45,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, EventDispatcher $dispatcher = null, array $drivers = null, VersionCacheInterface $versionCache = null) { @@ -125,6 +126,11 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt return $this->emptyReferences; } + public function getVersionTransportExceptions() + { + return $this->versionTransportExceptions; + } + protected function initialize() { parent::initialize(); @@ -226,11 +232,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; } @@ -306,11 +315,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) { From beacdf4e51b1e962acbda93aef931e5e81844ef3 Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Fri, 20 Dec 2019 18:59:49 +0000 Subject: [PATCH 06/13] Add Windows OneDrive to platform warnings --- src/Composer/Command/DiagnoseCommand.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index 19ed81392..9bc271fa1 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -576,6 +576,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) { @@ -699,6 +706,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'); } From 3d8c1ebad5862fbfc1885bd8b0b806e14edf1c57 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 21 Dec 2019 19:26:29 +0100 Subject: [PATCH 07/13] implemented @putenv composer script @putenv supports setting environment variables in a x-OS compatible way --- .../EventDispatcher/EventDispatcher.php | 8 +++- .../EventDispatcher/EventDispatcherTest.php | 37 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index d5bdb4c97..c623ada42 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -244,7 +244,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(); @@ -512,7 +516,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/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index 5e68ebcc9..3d5e0c712 100644 --- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -230,6 +230,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(); From b82a96405d6681d74beb4bc75fd8a5ce1f853461 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sun, 22 Dec 2019 12:32:04 +0100 Subject: [PATCH 08/13] Add @putenv to docs, refs #8492 --- doc/articles/scripts.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/doc/articles/scripts.md b/doc/articles/scripts.md index 38f6861b6..d51d01c75 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 `@setenv`: + +```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`: From 2ff079ba22f2b060ed4ae62b314dbc085df587bc Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 22 Dec 2019 12:33:58 +0100 Subject: [PATCH 09/13] Fix typo --- doc/articles/scripts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/articles/scripts.md b/doc/articles/scripts.md index d51d01c75..5524eba76 100644 --- a/doc/articles/scripts.md +++ b/doc/articles/scripts.md @@ -344,7 +344,7 @@ 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 `@setenv`: +To set an environment variable in a cross-platform way, you can use `@putenv`: ```json { From 6827105a5b2b7b77202db8fe63a33c67682939cd Mon Sep 17 00:00:00 2001 From: Tyson Andre Date: Sun, 29 Dec 2019 12:14:38 -0500 Subject: [PATCH 10/13] Fix nits on typos --- doc/01-basic-usage.md | 4 ++-- src/Composer/Downloader/SvnDownloader.php | 6 +++--- src/Composer/Util/Hg.php | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) 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/src/Composer/Downloader/SvnDownloader.php b/src/Composer/Downloader/SvnDownloader.php index e23958164..f7e14e6ce 100644 --- a/src/Composer/Downloader/SvnDownloader.php +++ b/src/Composer/Downloader/SvnDownloader.php @@ -130,11 +130,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/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); From c5873e11f20fcdc7d9abab69dda1f8d2a1b2c03a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 13 Jan 2020 12:06:21 +0100 Subject: [PATCH 11/13] Update deps --- composer.lock | 115 +++++++++++++++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 44 deletions(-) diff --git a/composer.lock b/composer.lock index 0580c0b1b..4b90652c6 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": { @@ -60,28 +60,27 @@ "ssl", "tls" ], - "time": "2019-08-30T08:44:50+00:00" + "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": { @@ -122,7 +121,7 @@ "validation", "versioning" ], - "time": "2019-03-19T17:25:45+00:00" + "time": "2020-01-13T12:06:48+00:00" }, { "name": "composer/spdx-licenses", @@ -186,24 +185,24 @@ }, { "name": "composer/xdebug-handler", - "version": "1.3.3", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f" + "reference": "cbe23383749496fe0f373345208b79568e4bc248" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/46867cbf8ca9fb8d60c506895449eb799db1184f", - "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/cbe23383749496fe0f373345208b79568e4bc248", + "reference": "cbe23383749496fe0f373345208b79568e4bc248", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0", + "php": "^5.3.2 || ^7.0 || ^8.0", "psr/log": "^1.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8" }, "type": "library", "autoload": { @@ -221,12 +220,17 @@ "email": "john-stevenson@blueyonder.co.uk" } ], - "description": "Restarts a process without xdebug.", + "description": "Restarts a process without Xdebug.", "keywords": [ "Xdebug", "performance" ], - "time": "2019-05-27T17:52:04+00:00" + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/1.4.0" + }, + "time": "2019-11-06T16:40:04+00:00" }, { "name": "justinrainbow/json-schema", @@ -292,6 +296,10 @@ "json", "schema" ], + "support": { + "issues": "https://github.com/justinrainbow/json-schema/issues", + "source": "https://github.com/justinrainbow/json-schema/tree/5.2.9" + }, "time": "2019-09-25T14:49:45+00:00" }, { @@ -339,6 +347,9 @@ "psr", "psr-3" ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.2" + }, "time": "2019-11-01T11:05:21+00:00" }, { @@ -388,20 +399,24 @@ "parser", "validator" ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.7.2" + }, "time": "2019-10-24T14:27:39+00:00" }, { "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": { @@ -432,11 +447,11 @@ "keywords": [ "phra" ], - "time": "2015-10-13T18:44:15+00:00" + "time": "2020-01-13T10:41:09+00:00" }, { "name": "symfony/console", - "version": "v2.8.50", + "version": "v2.8.52", "source": { "type": "git", "url": "https://github.com/symfony/console.git", @@ -493,11 +508,14 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/console/tree/v2.8.52" + }, "time": "2018-11-20T15:55:20+00:00" }, { "name": "symfony/debug", - "version": "v2.8.50", + "version": "v2.8.52", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", @@ -554,7 +572,7 @@ }, { "name": "symfony/filesystem", - "version": "v2.8.50", + "version": "v2.8.52", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", @@ -600,11 +618,14 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v2.8.52" + }, "time": "2018-11-11T11:18:13+00:00" }, { "name": "symfony/finder", - "version": "v2.8.50", + "version": "v2.8.52", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", @@ -649,20 +670,23 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v2.8.50" + }, "time": "2018-11-11T11:18:13+00:00" }, { "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": { @@ -674,7 +698,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.12-dev" + "dev-master": "1.13-dev" } }, "autoload": { @@ -707,20 +731,20 @@ "polyfill", "portable" ], - "time": "2019-08-06T08:03:45+00:00" + "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": { @@ -732,7 +756,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.12-dev" + "dev-master": "1.13-dev" } }, "autoload": { @@ -766,11 +790,11 @@ "portable", "shim" ], - "time": "2019-08-06T08:03:45+00:00" + "time": "2019-11-27T14:18:11+00:00" }, { "name": "symfony/process", - "version": "v2.8.50", + "version": "v2.8.52", "source": { "type": "git", "url": "https://github.com/symfony/process.git", @@ -815,6 +839,9 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v2.8.50" + }, "time": "2018-11-11T11:18:13+00:00" } ], @@ -1736,7 +1763,7 @@ }, { "name": "symfony/yaml", - "version": "v2.8.50", + "version": "v2.8.52", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", From 7d4d4622ab109ef0d13115c93145b20e598ca212 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 11 Jan 2020 09:57:50 +0100 Subject: [PATCH 12/13] ProcessExecutor - do not format output from executed processes --- tests/Composer/Test/Util/ProcessExecutorTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Composer/Test/Util/ProcessExecutorTest.php b/tests/Composer/Test/Util/ProcessExecutorTest.php index db16b8c02..563fcf253 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', $output->fetch()); + } } From d3f1c664d41416ec3a16b4b425538b5916832467 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 13 Jan 2020 13:30:46 +0100 Subject: [PATCH 13/13] Avoid formatting output from external processes, fixes #8524 --- src/Composer/IO/BaseIO.php | 16 ++++++++++++ src/Composer/IO/ConsoleIO.php | 26 ++++++++++++++++++- src/Composer/Util/ProcessExecutor.php | 14 +++++++--- .../Test/Util/ProcessExecutorTest.php | 2 +- 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/Composer/IO/BaseIO.php b/src/Composer/IO/BaseIO.php index 8f61c863d..cb2e99def 100644 --- a/src/Composer/IO/BaseIO.php +++ b/src/Composer/IO/BaseIO.php @@ -65,6 +65,22 @@ abstract class BaseIO implements IOInterface, LoggerInterface $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/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/Util/ProcessExecutorTest.php b/tests/Composer/Test/Util/ProcessExecutorTest.php index 563fcf253..29723b4a5 100644 --- a/tests/Composer/Test/Util/ProcessExecutorTest.php +++ b/tests/Composer/Test/Util/ProcessExecutorTest.php @@ -111,6 +111,6 @@ class ProcessExecutorTest extends TestCase $process = new ProcessExecutor(new ConsoleIO(new ArrayInput([]), $output, new HelperSet([]))); $process->execute('echo \'foo\''); - $this->assertSame('foo', $output->fetch()); + $this->assertSame('foo'.PHP_EOL, $output->fetch()); } }