Browse Source

Merge branch 'master' into 2.0

Jordi Boggiano 5 years ago
parent
commit
23359f2db6

+ 59 - 31
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",

+ 2 - 2
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.

+ 15 - 0
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`:

+ 1 - 1
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",

+ 12 - 0
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');
             }

+ 3 - 3
src/Composer/Downloader/SvnDownloader.php

@@ -138,11 +138,11 @@ class SvnDownloader extends VcsDownloader
         $this->io->writeError(sprintf('    <error>The package has modified file%s:</error>', $countChanges === 1 ? '' : 's'));
         $this->io->writeError(array_slice($changes, 0, 10));
         if ($countChanges > 10) {
-            $remaingChanges = $countChanges - 10;
+            $remainingChanges = $countChanges - 10;
             $this->io->writeError(
                 sprintf(
-                    '    <info>'.$remaingChanges.' more file%s modified, choose "v" to view the full list</info>',
-                    $remaingChanges === 1 ? '' : 's'
+                    '    <info>'.$remainingChanges.' more file%s modified, choose "v" to view the full list</info>',
+                    $remainingChanges === 1 ? '' : 's'
                 )
             );
         }

+ 6 - 2
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);
     }
 
     /**

+ 16 - 0
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.
      *

+ 25 - 1
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;

+ 14 - 4
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('<warning>Skipped tag '.$tag.', '.($e instanceof TransportException ? 'no composer file was found' : $e->getMessage()).'</warning>');
+                    $this->io->writeError('<warning>Skipped tag '.$tag.', '.($e instanceof TransportException ? 'no composer file was found (' . $e->getCode() . ' HTTP status code)' : $e->getMessage()).'</warning>');
                 }
                 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('<warning>Skipped branch '.$branch.', no composer file was found</warning>');
+                    $this->io->writeError('<warning>Skipped branch '.$branch.', no composer file was found (' . $e->getCode() . ' HTTP status code)</warning>');
                 }
                 continue;
             } catch (\Exception $e) {

+ 1 - 1
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);

+ 360 - 72
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);
+
+        if ($err || !$this->ipCheckData($host, $ipdata)) {
+            return false;
+        }
+
+        return $this->makeData($host, $port, $ipdata);
+    }
+
+    /**
+     * Returns true if the url is matched by a rule
+     *
+     * @param int $index
+     * @param string $hostName
+     * @param string $url
+     *
+     * @return bool
+     */
+    protected function match($index, $hostName, $url)
+    {
+        if (!$rule = $this->getRule($index, $hostName)) {
+            // Data must have been misformatted
+            return false;
+        }
+
+        if ($rule->ipdata) {
+            // Match ipdata first
+            if (!$url->ipdata) {
+                return false;
             }
 
-            $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;
+            if ($rule->ipdata->netmask) {
+                return $this->matchRange($rule->ipdata, $url->ipdata);
             }
 
-            // final port check
-            if ($match && strpos($rule, ':') !== false) {
-                list(, $rulePort) = explode(':', $rule);
-                if (!empty($rulePort) && $port != $rulePort) {
-                    $match = false;
-                }
+            $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;
+        }
+
+        if ($match && $rule->port) {
+            $match = $rule->port === $url->port;
+        }
+
+        return $match;
+    }
+
+    /**
+     * 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);
+
+        for ($i = 1; $i < 17; ++$i) {
+            if (($net[$i] & $mask[$i]) !== ($ip[$i] & $mask[$i])) {
+                return false;
             }
+        }
 
-            if ($match) {
-                return true;
+        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;
         }
 
-        return false;
+        // 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;
     }
 
     /**
-     * Check an IP address against a CIDR
+     * Returns an array of the IP in_addr and its byte size
      *
-     * http://framework.zend.com/svn/framework/extras/incubator/library/ZendX/Whois/Adapter/Cidr.php
+     * IPv4 addresses are always mapped to IPv6, which simplifies handling
+     * and comparison.
      *
-     * @param string $cidr IPv4 block in CIDR notation
-     * @param string $ip   IPv4 address
+     * @param string $host
      *
-     * @return bool
+     * @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 static function inCIDRBlock($cidr, $ip)
+    private function ipGetNetwork($rangeIp, $size, $prefix)
     {
-        // Get the base and the bits from the CIDR
-        list($base, $bits) = explode('/', $cidr);
+        $netmask = $this->ipGetMask($prefix, $size);
 
-        // Now split it up into it's classes
-        list($a, $b, $c, $d) = explode('.', $base);
+        // Get the network from the address and mask
+        $mask = unpack('C*', $netmask);
+        $ip = unpack('C*', $rangeIp);
+        $net = '';
 
-        // 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));
+        for ($i = 1; $i < 17; ++$i) {
+            $net .= chr($ip[$i] & $mask[$i]);
+        }
 
-        // Here's our lowest int
-        $low = $i & $mask;
+        return array($net, $netmask);
+    }
 
-        // Here's our highest int
-        $high = $i | (~$mask & 0xFFFFFFFF);
+    /**
+     * 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;
+        }
 
-        // Now split the ip we're checking against up into classes
-        list($a, $b, $c, $d) = explode('.', $ip);
+        return $binary;
+    }
 
-        // Now convert the ip we're checking against to an int
-        $check = ($a << 24) + ($b << 16) + ($c << 8) + $d;
+    /**
+     * 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)
+        );
 
-        // If the ip is within the range, including highest/lowest values,
-        // then it's within the CIDR range
-        return $check >= $low && $check <= $high;
+        return false !== filter_var($int, FILTER_VALIDATE_INT, $options);
     }
 }

+ 11 - 3
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);
+            }
         }
     }
 

+ 37 - 0
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();

+ 143 - 0
tests/Composer/Test/Util/NoProxyPatternTest.php

@@ -0,0 +1,143 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\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);
+    }
+}

+ 14 - 0
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 \'<error>foo</error>\'');
+        $this->assertSame('<error>foo</error>'.PHP_EOL, $output->fetch());
+    }
 }