From e9cac53f907aa80396e8dca6c25e0bd6b494bc4e Mon Sep 17 00:00:00 2001 From: Oliver Vartiainen Date: Tue, 27 Oct 2015 19:47:30 +0200 Subject: [PATCH 01/98] Allow fetching auth credentials from an envvar When an environmental variable named "COMPOSER_AUTH" is set as $USERNAME:$PASSWORD, it is automatically used for authentication e.g. when fetching packages from Satis. The envvar credentials are of lower priority than URL credentials. Fixes #4285 --- src/Composer/Util/RemoteFilesystem.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 4754e304b..7c0e3e76c 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -207,6 +207,16 @@ class RemoteFilesystem $this->retryAuthFailure = true; $this->lastHeaders = array(); + // Use COMPOSER_AUTH environment variable if set + if (getenv('COMPOSER_AUTH')) { + $credentials = []; + preg_match('/(.+):(.+)/', getenv('COMPOSER_AUTH'), $credentials); + + if (count($credentials) === 2) { + $this->io->setAuthentication($originUrl, $credentials[0], $credentials[1]); + } + } + // capture username/password from URL if there is one if (preg_match('{^https?://(.+):(.+)@([^/]+)}i', $fileUrl, $match)) { $this->io->setAuthentication($originUrl, urldecode($match[1]), urldecode($match[2])); From aaee6dc0b08bbe800fab0be2b91fc5113c4a995d Mon Sep 17 00:00:00 2001 From: Oliver Vartiainen Date: Tue, 27 Oct 2015 20:44:10 +0200 Subject: [PATCH 02/98] Simplify envvar credential parsing --- src/Composer/Util/RemoteFilesystem.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 7c0e3e76c..738bd36c4 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -209,8 +209,7 @@ class RemoteFilesystem // Use COMPOSER_AUTH environment variable if set if (getenv('COMPOSER_AUTH')) { - $credentials = []; - preg_match('/(.+):(.+)/', getenv('COMPOSER_AUTH'), $credentials); + $credentials = explode(':', getenv('COMPOSER_AUTH'), 2); if (count($credentials) === 2) { $this->io->setAuthentication($originUrl, $credentials[0], $credentials[1]); From b39b113fc3b4376d5c6519a18143f45d8c367086 Mon Sep 17 00:00:00 2001 From: Oliver Vartiainen Date: Tue, 19 Jan 2016 20:34:04 +0200 Subject: [PATCH 03/98] Handle envvar auth credentials as a JSON blob As well as move the handling to a proper place --- src/Composer/IO/BaseIO.php | 13 +++++++++++++ src/Composer/Util/RemoteFilesystem.php | 9 --------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Composer/IO/BaseIO.php b/src/Composer/IO/BaseIO.php index 139e58723..43248c92c 100644 --- a/src/Composer/IO/BaseIO.php +++ b/src/Composer/IO/BaseIO.php @@ -60,6 +60,19 @@ abstract class BaseIO implements IOInterface */ public function loadConfiguration(Config $config) { + // Use COMPOSER_AUTH environment variable if set + if ($envvar_data = getenv('COMPOSER_AUTH')) { + $auth_data = json_decode($envvar_data); + + if (is_null($auth_data)) { + throw new \UnexpectedValueException('COMPOSER_AUTH environment variable is malformed'); + } + + foreach ($auth_data as $domain => $credentials) { + $this->setAuthentication($domain, $credentials->username, $credentials->password); + } + } + // reload oauth token from config if available if ($tokens = $config->get('github-oauth')) { foreach ($tokens as $domain => $token) { diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 738bd36c4..4754e304b 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -207,15 +207,6 @@ class RemoteFilesystem $this->retryAuthFailure = true; $this->lastHeaders = array(); - // Use COMPOSER_AUTH environment variable if set - if (getenv('COMPOSER_AUTH')) { - $credentials = explode(':', getenv('COMPOSER_AUTH'), 2); - - if (count($credentials) === 2) { - $this->io->setAuthentication($originUrl, $credentials[0], $credentials[1]); - } - } - // capture username/password from URL if there is one if (preg_match('{^https?://(.+):(.+)@([^/]+)}i', $fileUrl, $match)) { $this->io->setAuthentication($originUrl, urldecode($match[1]), urldecode($match[2])); From 09417cae504dfecb87a92a59113c1d98fb37f6de Mon Sep 17 00:00:00 2001 From: Radek Benkel Date: Sun, 20 Jul 2014 21:01:17 +0200 Subject: [PATCH 04/98] Composer gives .ini hints about missing extensions --- .../SolverProblemsException.php | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index 9973f9d39..1dfc116f0 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -31,14 +31,23 @@ class SolverProblemsException extends \RuntimeException protected function createMessage() { $text = "\n"; + $hasExtensionProblems = false; foreach ($this->problems as $i => $problem) { $text .= " Problem ".($i + 1).$problem->getPrettyString($this->installedMap)."\n"; + + if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) { + $hasExtensionProblems = true; + } } if (strpos($text, 'could not be found') || strpos($text, 'no matching package found')) { $text .= "\nPotential causes:\n - A typo in the package name\n - The package is not available in a stable-enough version according to your minimum-stability setting\n see for more details.\n\nRead for further common problems."; } + if ($hasExtensionProblems) { + $text .= $this->createExtensionHint(); + } + return $text; } @@ -46,4 +55,40 @@ class SolverProblemsException extends \RuntimeException { return $this->problems; } + + private function createExtensionHint() + { + $paths = array(); + + if (($iniPath = php_ini_loaded_file()) !== false) { + $paths[] = $iniPath; + } + + if (!defined('HHVM_VERSION') && $additionalIniPaths = php_ini_scanned_files()) { + $paths = array_merge($paths, array_map("trim", explode(",", $additionalIniPaths))); + } + + if (count($paths) === 0) { + return ''; + } + + $text = "\n Because of missing extensions, please verify whether they are enabled in those .ini files:\n - "; + $text .= implode("\n - ", $paths); + $text .= "\n You can also run `php --ini` inside terminal to see which files are used by PHP in CLI mode."; + + return $text; + } + + private function hasExtensionProblems(array $reasonSets) + { + foreach ($reasonSets as $reasonSet) { + foreach($reasonSet as $reason) { + if (isset($reason["rule"]) && 0 === strpos($reason["rule"]->getRequiredPackage(), 'ext-')) { + return true; + } + } + } + + return false; + } } From 73662c725ab072e6750077d1c4a856221bde2a5e Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sun, 17 Jan 2016 19:15:06 +0000 Subject: [PATCH 05/98] Don't let PHP follow redirects it doesn't validate certificates --- src/Composer/Util/RemoteFilesystem.php | 78 +++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 4754e304b..b317a2399 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -38,6 +38,8 @@ class RemoteFilesystem private $lastHeaders; private $storeAuth; private $degradedMode = false; + private $redirects = 0; + private $maxRedirects = 20; /** * Constructor. @@ -293,6 +295,72 @@ class RemoteFilesystem $statusCode = $this->findStatusCode($http_response_header); } + if (!empty($http_response_header[0]) && preg_match('{^HTTP/\S+ (3\d\d)}i', $http_response_header[0], $match)) { + // TODO: Only follow if PHP is not set to follow. + foreach ($http_response_header as $header) { + if (preg_match('{^location: *(.+) *$}i', $header, $m)) { + if (parse_url($m[1], PHP_URL_SCHEME)) { + $targetUrl = $m[1]; + + break; + } + + if (parse_url($m[1], PHP_URL_HOST)) { + // Scheme relative; e.g. //example.com/foo + $targetUrl = $this->scheme.':'.$m[1]; + break; + } + + if ('/' === $m[1][0]) { + // Absolute path; e.g. /foo + throw new \Exception('todo'); + } + + throw new \Exception('todo'); + break; + } + } + + if ($targetUrl) { + $this->redirects++; + + if ($this->redirects > $this->maxRedirects) { + $e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded, too many redirects'); + $e->setHeaders($http_response_header); + $e->setResponse($result); + throw $e; + } + + if ('http' === parse_url($targetUrl, PHP_URL_SCHEME) && 'https' === $this->scheme) { + // Do not allow protocol downgrade. + // TODO: Currently this will fail if a request goes http -> https -> http + $e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded, not permitting protocol downgrade'); + $e->setHeaders($http_response_header); + $e->setResponse($result); + throw $e; + } + + if ($this->io->isDebug()) { + $this->io->writeError(sprintf('Following redirect (%u)', $this->redirects)); + } + + // TODO: Not so sure about preserving origin here... + $result = $this->get($this->originUrl, $targetUrl, $additionalOptions, $this->fileName, $this->progress); + + $this->redirects--; + + return $result; + } + + if (!$this->retry) { + $e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded, got redirect without Location ('.$http_response_header[0].')'); + $e->setHeaders($http_response_header); + $e->setResponse($result); + throw $e; + } + $result = false; + } + // fail 4xx and 5xx responses and capture the response if ($statusCode && $statusCode >= 400 && $statusCode <= 599) { if (!$this->retry) { @@ -529,9 +597,9 @@ class RemoteFilesystem $host = parse_url($this->fileUrl, PHP_URL_HOST); } - if ($host === 'github.com' || $host === 'api.github.com') { - $host = '*.github.com'; - } + // if ($host === 'github.com' || $host === 'api.github.com') { + // $host = '*.github.com'; + // } $tlsOptions['ssl']['CN_match'] = $host; $tlsOptions['ssl']['SNI_server_name'] = $host; @@ -551,6 +619,10 @@ class RemoteFilesystem $headers[] = 'Connection: close'; } + if (PHP_VERSION_ID >= 50304 && $this->disableTls === false) { + $options['http']['follow_location'] = 0; + } + if ($this->io->hasAuthentication($originUrl)) { $auth = $this->io->getAuthentication($originUrl); if ('github.com' === $originUrl && 'x-oauth-basic' === $auth['password']) { From ce1eda25f3b7895531a0a7845377fc8f9a17031c Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sun, 17 Jan 2016 19:42:43 +0000 Subject: [PATCH 06/98] Follow redirects inside RFS only when required by PHP version --- src/Composer/Util/RemoteFilesystem.php | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index b317a2399..ba40517c4 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -221,6 +221,7 @@ class RemoteFilesystem } $options = $this->getOptionsForUrl($originUrl, $additionalOptions); + $userlandFollow = isset($options['http']['follow_location']) && !$options['http']['follow_location']; if ($this->io->isDebug()) { $this->io->writeError((substr($fileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $fileUrl); @@ -295,8 +296,7 @@ class RemoteFilesystem $statusCode = $this->findStatusCode($http_response_header); } - if (!empty($http_response_header[0]) && preg_match('{^HTTP/\S+ (3\d\d)}i', $http_response_header[0], $match)) { - // TODO: Only follow if PHP is not set to follow. + if ($userlandFollow && !empty($http_response_header[0]) && preg_match('{^HTTP/\S+ (3\d\d)}i', $http_response_header[0], $match)) { foreach ($http_response_header as $header) { if (preg_match('{^location: *(.+) *$}i', $header, $m)) { if (parse_url($m[1], PHP_URL_SCHEME)) { @@ -597,9 +597,20 @@ class RemoteFilesystem $host = parse_url($this->fileUrl, PHP_URL_HOST); } - // if ($host === 'github.com' || $host === 'api.github.com') { - // $host = '*.github.com'; - // } + if (PHP_VERSION_ID >= 50304) { + // Must manually follow when setting CN_match because this causes all + // redirects to be validated against the same CN_match value. + $userlandFollow = true; + } else { + // PHP < 5.3.4 does not support follow_location, for those people + // do some really nasty hard coded transformations. These will + // still breakdown if the site redirects to a domain we don't + // expect. + + if ($host === 'github.com' || $host === 'api.github.com') { + $host = '*.github.com'; + } + } $tlsOptions['ssl']['CN_match'] = $host; $tlsOptions['ssl']['SNI_server_name'] = $host; @@ -619,7 +630,7 @@ class RemoteFilesystem $headers[] = 'Connection: close'; } - if (PHP_VERSION_ID >= 50304 && $this->disableTls === false) { + if (isset($userlandFollow)) { $options['http']['follow_location'] = 0; } From ffab235edd5cc05ba7fe37d0394cd929dc270f65 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sun, 17 Jan 2016 19:43:08 +0000 Subject: [PATCH 07/98] Remove code preventing protocol downgrades --- src/Composer/Util/RemoteFilesystem.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index ba40517c4..7ea71c2b0 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -331,14 +331,15 @@ class RemoteFilesystem throw $e; } - if ('http' === parse_url($targetUrl, PHP_URL_SCHEME) && 'https' === $this->scheme) { - // Do not allow protocol downgrade. - // TODO: Currently this will fail if a request goes http -> https -> http - $e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded, not permitting protocol downgrade'); - $e->setHeaders($http_response_header); - $e->setResponse($result); - throw $e; - } + // TODO: Disabled because this is (probably) different behaviour to PHP following for us. + // if ('http' === parse_url($targetUrl, PHP_URL_SCHEME) && 'https' === $this->scheme) { + // // Do not allow protocol downgrade. + // // TODO: Currently this will fail if a request goes http -> https -> http + // $e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded, not permitting protocol downgrade'); + // $e->setHeaders($http_response_header); + // $e->setResponse($result); + // throw $e; + // } if ($this->io->isDebug()) { $this->io->writeError(sprintf('Following redirect (%u)', $this->redirects)); From e830a611ec620237534822991675a3da3018bbf0 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sun, 17 Jan 2016 21:17:52 +0000 Subject: [PATCH 08/98] Handle other path redirects --- src/Composer/Util/RemoteFilesystem.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 7ea71c2b0..ef2a91e26 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -313,10 +313,16 @@ class RemoteFilesystem if ('/' === $m[1][0]) { // Absolute path; e.g. /foo - throw new \Exception('todo'); + $urlHost = parse_url($this->fileUrl, PHP_URL_HOST); + + // Replace path using hostname as an anchor. + $targetUrl = preg_replace('{^(.+(?://|@)'.preg_quote($urlHost).')(?:[/\?].*)?$}', '\1'.$m[1], $this->fileUrl); } - throw new \Exception('todo'); + // Relative path; e.g. foo + // This actually differs from PHP which seems to add duplicate slashes. + $targetUrl = preg_replace('{^(.+/)[^/?]*(?:\?.*)?$}', '\1'.$m[1], $this->fileUrl); + break; } } From 33471e389f126de413619b53afc4520264ccda3a Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sun, 17 Jan 2016 21:33:55 +0000 Subject: [PATCH 09/98] Pass redirect count using options Removing the risk it might be preserved between requests. --- src/Composer/Util/RemoteFilesystem.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index ef2a91e26..1216761ec 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -38,7 +38,7 @@ class RemoteFilesystem private $lastHeaders; private $storeAuth; private $degradedMode = false; - private $redirects = 0; + private $redirects; private $maxRedirects = 20; /** @@ -208,6 +208,7 @@ class RemoteFilesystem $this->lastProgress = null; $this->retryAuthFailure = true; $this->lastHeaders = array(); + $this->redirects = 1; // The first request counts. // capture username/password from URL if there is one if (preg_match('{^https?://(.+):(.+)@([^/]+)}i', $fileUrl, $match)) { @@ -220,6 +221,12 @@ class RemoteFilesystem unset($additionalOptions['retry-auth-failure']); } + if (isset($additionalOptions['redirects'])) { + $this->redirects = $additionalOptions['redirects']; + + unset($additionalOptions['redirects']); + } + $options = $this->getOptionsForUrl($originUrl, $additionalOptions); $userlandFollow = isset($options['http']['follow_location']) && !$options['http']['follow_location']; @@ -351,12 +358,10 @@ class RemoteFilesystem $this->io->writeError(sprintf('Following redirect (%u)', $this->redirects)); } - // TODO: Not so sure about preserving origin here... - $result = $this->get($this->originUrl, $targetUrl, $additionalOptions, $this->fileName, $this->progress); - - $this->redirects--; + $additionalOptions['redirects'] = $this->redirects; - return $result; + // TODO: Not so sure about preserving origin here... + return $this->get($this->originUrl, $targetUrl, $additionalOptions, $this->fileName, $this->progress); } if (!$this->retry) { From 8a8ec6fccc1cdfed76a30080811371966df2129e Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sun, 17 Jan 2016 21:34:34 +0000 Subject: [PATCH 10/98] Too many redirects is not an error in PHP, return the latest response --- src/Composer/Util/RemoteFilesystem.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 1216761ec..82b8f400d 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -303,7 +303,7 @@ class RemoteFilesystem $statusCode = $this->findStatusCode($http_response_header); } - if ($userlandFollow && !empty($http_response_header[0]) && preg_match('{^HTTP/\S+ (3\d\d)}i', $http_response_header[0], $match)) { + if ($userlandFollow && !empty($http_response_header[0]) && preg_match('{^HTTP/\S+ (3\d\d)}i', $http_response_header[0], $match) && $this->redirects < $this->maxRedirects) { foreach ($http_response_header as $header) { if (preg_match('{^location: *(.+) *$}i', $header, $m)) { if (parse_url($m[1], PHP_URL_SCHEME)) { @@ -337,13 +337,6 @@ class RemoteFilesystem if ($targetUrl) { $this->redirects++; - if ($this->redirects > $this->maxRedirects) { - $e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded, too many redirects'); - $e->setHeaders($http_response_header); - $e->setResponse($result); - throw $e; - } - // TODO: Disabled because this is (probably) different behaviour to PHP following for us. // if ('http' === parse_url($targetUrl, PHP_URL_SCHEME) && 'https' === $this->scheme) { // // Do not allow protocol downgrade. From dd3216e93d43e48a0bd82c82fd629c6c761b8d66 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 19 Jan 2016 22:19:17 +0000 Subject: [PATCH 11/98] Refactor to use new helper methods for headers --- src/Composer/Util/RemoteFilesystem.php | 44 ++++++++++---------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 82b8f400d..6ecc349db 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -303,38 +303,28 @@ class RemoteFilesystem $statusCode = $this->findStatusCode($http_response_header); } - if ($userlandFollow && !empty($http_response_header[0]) && preg_match('{^HTTP/\S+ (3\d\d)}i', $http_response_header[0], $match) && $this->redirects < $this->maxRedirects) { - foreach ($http_response_header as $header) { - if (preg_match('{^location: *(.+) *$}i', $header, $m)) { - if (parse_url($m[1], PHP_URL_SCHEME)) { - $targetUrl = $m[1]; - - break; - } - - if (parse_url($m[1], PHP_URL_HOST)) { - // Scheme relative; e.g. //example.com/foo - $targetUrl = $this->scheme.':'.$m[1]; - break; - } - - if ('/' === $m[1][0]) { - // Absolute path; e.g. /foo - $urlHost = parse_url($this->fileUrl, PHP_URL_HOST); - - // Replace path using hostname as an anchor. - $targetUrl = preg_replace('{^(.+(?://|@)'.preg_quote($urlHost).')(?:[/\?].*)?$}', '\1'.$m[1], $this->fileUrl); - } - + if ($userlandFollow && $statusCode >= 300 && $statusCode <= 399 && $this->redirects < $this->maxRedirects) { + if ($locationHeader = $this->findHeaderValue($http_response_header, 'location')) { + if (parse_url($locationHeader, PHP_URL_SCHEME)) { + // Absolute URL; e.g. https://example.com/composer + $targetUrl = $locationHeader; + } elseif (parse_url($locationHeader, PHP_URL_HOST)) { + // Scheme relative; e.g. //example.com/foo + $targetUrl = $this->scheme.':'.$locationHeader; + } elseif ('/' === $locationHeader[0]) { + // Absolute path; e.g. /foo + $urlHost = parse_url($this->fileUrl, PHP_URL_HOST); + + // Replace path using hostname as an anchor. + $targetUrl = preg_replace('{^(.+(?://|@)'.preg_quote($urlHost).')(?:[/\?].*)?$}', '\1'.$locationHeader, $this->fileUrl); + } else { // Relative path; e.g. foo // This actually differs from PHP which seems to add duplicate slashes. - $targetUrl = preg_replace('{^(.+/)[^/?]*(?:\?.*)?$}', '\1'.$m[1], $this->fileUrl); - - break; + $targetUrl = preg_replace('{^(.+/)[^/?]*(?:\?.*)?$}', '\1'.$locationHeader, $this->fileUrl); } } - if ($targetUrl) { + if (!empty($targetUrl)) { $this->redirects++; // TODO: Disabled because this is (probably) different behaviour to PHP following for us. From c1488f65bf7c91f44f5ad960c8c10212b3c6e4bf Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Wed, 20 Jan 2016 21:20:18 +0100 Subject: [PATCH 12/98] a quick stab at adding capath --- doc/06-config.md | 12 ++++++--- src/Composer/Command/ConfigCommand.php | 4 +++ src/Composer/Config.php | 2 ++ src/Composer/Factory.php | 12 +++++++-- src/Composer/Util/RemoteFilesystem.php | 35 ++++++++++---------------- 5 files changed, 38 insertions(+), 27 deletions(-) diff --git a/doc/06-config.md b/doc/06-config.md index 59390ea42..f6c2da532 100644 --- a/doc/06-config.md +++ b/doc/06-config.md @@ -55,9 +55,15 @@ php_openssl extension in php.ini. ## cafile -A way to set the path to the openssl CA file. In PHP 5.6+ you should rather -set this via openssl.cafile in php.ini, although PHP 5.6+ should be able to -detect your system CA file automatically. +Location of Certificate Authority file on local filesystem. In PHP 5.6+ you +should rather set this via openssl.cafile in php.ini, although PHP 5.6+ should +be able to detect your system CA file automatically. + +## capath + +If cafile is not specified or if the certificate is not found there, the +directory pointed to by capath is searched for a suitable certificate. +capath must be a correctly hashed certificate directory. ## http-basic diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index 8378bc5b4..0bf8f97d5 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -333,6 +333,10 @@ EOT function ($val) { return file_exists($val) && is_readable($val); }, function ($val) { return $val === 'null' ? null : $val; } ), + 'capath' => array( + function ($val) { return is_dir($val) && is_readable($val); }, + function ($val) { return $val === 'null' ? null : $val; } + ), 'github-expose-hostname' => array($booleanValidator, $booleanNormalizer), ); $multiConfigValues = array( diff --git a/src/Composer/Config.php b/src/Composer/Config.php index c7d8efa8e..a1b0246b9 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -47,6 +47,7 @@ class Config 'github-domains' => array('github.com'), 'disable-tls' => false, 'cafile' => null, + 'capath' => null, 'github-expose-hostname' => true, 'gitlab-domains' => array('gitlab.com'), 'store-auths' => 'prompt', @@ -179,6 +180,7 @@ class Config case 'cache-repo-dir': case 'cache-vcs-dir': case 'cafile': + case 'capath': // convert foo-bar to COMPOSER_FOO_BAR and check if it exists since it overrides the local config $env = 'COMPOSER_' . strtoupper(strtr($key, '-', '_')); diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 60431229b..042445a2a 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -590,9 +590,17 @@ class Factory $remoteFilesystemOptions = array(); if ($disableTls === false) { if ($config && $config->get('cafile')) { - $remoteFilesystemOptions = array('ssl' => array('cafile' => $config->get('cafile'))); + $remoteFilesystemOptions = array_merge_recursive( + $remoteFilesystemOptions, + array('ssl' => array('cafile' => $config->get('cafile'))) + ); + } + if ($config && $config->get('capath')) { + $remoteFilesystemOptions = array_merge_recursive( + $remoteFilesystemOptions, + array('ssl' => array('capath' => $config->get('capath'))) + ); } - $remoteFilesystemOptions = array_merge_recursive($remoteFilesystemOptions, $options); } try { $remoteFilesystem = new RemoteFilesystem($io, $config, $remoteFilesystemOptions, $disableTls); diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 4754e304b..48551be1b 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -54,15 +54,7 @@ class RemoteFilesystem // Setup TLS options // The cafile option can be set via config.json if ($disableTls === false) { - $this->options = $this->getTlsDefaults(); - if (isset($options['ssl']['cafile']) - && ( - !is_readable($options['ssl']['cafile']) - || !$this->validateCaFile($options['ssl']['cafile']) - ) - ) { - throw new TransportException('The configured cafile was not valid or could not be read.'); - } + $this->options = $this->getTlsDefaults($options); } else { $this->disableTls = true; } @@ -575,7 +567,7 @@ class RemoteFilesystem return $options; } - private function getTlsDefaults() + private function getTlsDefaults(array $options) { $ciphers = implode(':', array( 'ECDHE-RSA-AES128-GCM-SHA256', @@ -622,7 +614,7 @@ class RemoteFilesystem * * cafile or capath can be overridden by passing in those options to constructor. */ - $options = array( + $defaults = array( 'ssl' => array( 'ciphers' => $ciphers, 'verify_peer' => true, @@ -635,7 +627,7 @@ class RemoteFilesystem * Attempt to find a local cafile or throw an exception if none pre-set * The user may go download one if this occurs. */ - if (!isset($this->options['ssl']['cafile'])) { + if (!isset($options['ssl']['cafile']) && !isset($options['ssl']['capath'])) { $result = $this->getSystemCaRootBundlePath(); if ($result) { if (preg_match('{^phar://}', $result)) { @@ -659,7 +651,7 @@ class RemoteFilesystem } } } else { - throw new TransportException('A valid cafile could not be located automatically.'); + throw new TransportException('A valid cafile or capath could not be located automatically.'); } } @@ -667,10 +659,10 @@ class RemoteFilesystem * Disable TLS compression to prevent CRIME attacks where supported. */ if (PHP_VERSION_ID >= 50413) { - $options['ssl']['disable_compression'] = true; + $defaults['ssl']['disable_compression'] = true; } - return $options; + return $defaults; } /** @@ -721,6 +713,11 @@ class RemoteFilesystem return $caPath = $envCertFile; } + $configured = ini_get('openssl.cafile'); + if ($configured && strlen($configured) > 0 && is_readable($configured) && $this->validateCaFile($configured)) { + return $caPath = $configured; + } + $caBundlePaths = array( '/etc/pki/tls/certs/ca-bundle.crt', // Fedora, RHEL, CentOS (ca-certificates package) '/etc/ssl/certs/ca-certificates.crt', // Debian, Ubuntu, Gentoo, Arch Linux (ca-certificates package) @@ -732,14 +729,8 @@ class RemoteFilesystem '/usr/share/ssl/certs/ca-bundle.crt', // Really old RedHat? '/etc/ssl/cert.pem', // OpenBSD '/usr/local/etc/ssl/cert.pem', // FreeBSD 10.x - __DIR__.'/../../../res/cacert.pem', // Bundled with Composer ); - $configured = ini_get('openssl.cafile'); - if ($configured && strlen($configured) > 0 && is_readable($configured) && $this->validateCaFile($configured)) { - return $caPath = $configured; - } - foreach ($caBundlePaths as $caBundle) { if (@is_readable($caBundle) && $this->validateCaFile($caBundle)) { return $caPath = $caBundle; @@ -753,7 +744,7 @@ class RemoteFilesystem } } - return $caPath = false; + return $caPath = __DIR__.'/../../../res/cacert.pem'; // Bundled with Composer, last resort } private function validateCaFile($filename) From 008cce8d859f577b87220d2a9b3da6bdce75faa0 Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Wed, 20 Jan 2016 21:24:13 +0100 Subject: [PATCH 13/98] add back sanity checks --- src/Composer/Util/RemoteFilesystem.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 48551be1b..610dcb32e 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -655,6 +655,14 @@ class RemoteFilesystem } } + if (isset($options['ssl']['cafile']) && (!is_readable($options['ssl']['cafile']) || !$this->validateCaFile($options['ssl']['cafile']))) { + throw new TransportException('The configured cafile was not valid or could not be read.'); + } + + if (isset($options['ssl']['capath']) && (!is_dir($options['ssl']['capath']) || !is_readable($options['ssl']['capath']))) { + throw new TransportException('The configured capath was not valid or could not be read.'); + } + /** * Disable TLS compression to prevent CRIME attacks where supported. */ From b95b0c2ab6b05bf9683f12bdf441555253795f68 Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Wed, 20 Jan 2016 21:27:26 +0100 Subject: [PATCH 14/98] wrong array --- src/Composer/Util/RemoteFilesystem.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 610dcb32e..afb8820a9 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -642,12 +642,12 @@ class RemoteFilesystem fclose($target); unset($source, $target); - $options['ssl']['cafile'] = $targetPath; + $defaults['ssl']['cafile'] = $targetPath; } else { if (is_dir($result)) { - $options['ssl']['capath'] = $result; + $defaults['ssl']['capath'] = $result; } elseif ($result) { - $options['ssl']['cafile'] = $result; + $defaults['ssl']['cafile'] = $result; } } } else { @@ -655,11 +655,11 @@ class RemoteFilesystem } } - if (isset($options['ssl']['cafile']) && (!is_readable($options['ssl']['cafile']) || !$this->validateCaFile($options['ssl']['cafile']))) { + if (isset($defaults['ssl']['cafile']) && (!is_readable($defaults['ssl']['cafile']) || !$this->validateCaFile($defaults['ssl']['cafile']))) { throw new TransportException('The configured cafile was not valid or could not be read.'); } - if (isset($options['ssl']['capath']) && (!is_dir($options['ssl']['capath']) || !is_readable($options['ssl']['capath']))) { + if (isset($defaults['ssl']['capath']) && (!is_dir($defaults['ssl']['capath']) || !is_readable($defaults['ssl']['capath']))) { throw new TransportException('The configured capath was not valid or could not be read.'); } From 94947ee772948de260cb6638ecfd60f5bf173f2c Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Wed, 20 Jan 2016 21:29:55 +0100 Subject: [PATCH 15/98] merge isset() calls --- src/Composer/Util/RemoteFilesystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index afb8820a9..5f2d0bfa5 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -627,7 +627,7 @@ class RemoteFilesystem * Attempt to find a local cafile or throw an exception if none pre-set * The user may go download one if this occurs. */ - if (!isset($options['ssl']['cafile']) && !isset($options['ssl']['capath'])) { + if (!isset($options['ssl']['cafile'], $options['ssl']['capath'])) { $result = $this->getSystemCaRootBundlePath(); if ($result) { if (preg_match('{^phar://}', $result)) { From f79255df29503eca455c2cf045cd332c805b3730 Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Wed, 20 Jan 2016 21:35:06 +0100 Subject: [PATCH 16/98] make sure passed options are merged into defaults before checking --- src/Composer/Util/RemoteFilesystem.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 5f2d0bfa5..da5dcd25f 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -623,6 +623,10 @@ class RemoteFilesystem ) ); + if (isset($options['ssl'])) { + $defaults['ssl'] = array_merge_recursive($defaults['ssl'], $options['ssl']); + } + /** * Attempt to find a local cafile or throw an exception if none pre-set * The user may go download one if this occurs. From 4482a1dca08a2454549269a3d6cd30aa6d2442d9 Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Wed, 20 Jan 2016 21:53:49 +0100 Subject: [PATCH 17/98] also wrong array --- src/Composer/Util/RemoteFilesystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index da5dcd25f..d3c6c565c 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -631,7 +631,7 @@ class RemoteFilesystem * Attempt to find a local cafile or throw an exception if none pre-set * The user may go download one if this occurs. */ - if (!isset($options['ssl']['cafile'], $options['ssl']['capath'])) { + if (!isset($defaults['ssl']['cafile'], $defaults['ssl']['capath'])) { $result = $this->getSystemCaRootBundlePath(); if ($result) { if (preg_match('{^phar://}', $result)) { From 446f1b3e3164c5fac1d4bae8f5e52383643c5af5 Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Thu, 21 Jan 2016 10:22:12 +0100 Subject: [PATCH 18/98] fix zip test --- tests/Composer/Test/Downloader/ZipDownloaderTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index cb5a56569..07a358fb9 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -64,6 +64,10 @@ class ZipDownloaderTest extends \PHPUnit_Framework_TestCase ->with('cafile') ->will($this->returnValue(null)); $config->expects($this->at(2)) + ->method('get') + ->with('capath') + ->will($this->returnValue(null)); + $config->expects($this->at(3)) ->method('get') ->with('vendor-dir') ->will($this->returnValue($this->testDir)); From cef97904d00860702ee01b7a8ed126ddcee03e2f Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Thu, 21 Jan 2016 15:07:51 +0100 Subject: [PATCH 19/98] dont rewrite temp CA file if it already exists and make it readable by everyone the first time we create it --- src/Composer/Util/RemoteFilesystem.php | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index d3c6c565c..58e95d53e 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -633,10 +633,15 @@ class RemoteFilesystem */ if (!isset($defaults['ssl']['cafile'], $defaults['ssl']['capath'])) { $result = $this->getSystemCaRootBundlePath(); - if ($result) { - if (preg_match('{^phar://}', $result)) { - $targetPath = rtrim(sys_get_temp_dir(), '\\/') . '/composer-cacert.pem'; + if (!$result) { + throw new TransportException('A valid cafile or capath could not be located automatically.'); + } + + if (preg_match('{^phar://}', $result)) { + $targetPath = rtrim(sys_get_temp_dir(), '\\/') . '/composer-cacert.pem'; + + if (!file_exists($targetPath)) { // use stream_copy_to_stream instead of copy // to work around https://bugs.php.net/bug.php?id=64634 $source = fopen($result, 'r'); @@ -644,18 +649,15 @@ class RemoteFilesystem stream_copy_to_stream($source, $target); fclose($source); fclose($target); + chmod($targetPath, 0744); unset($source, $target); - - $defaults['ssl']['cafile'] = $targetPath; - } else { - if (is_dir($result)) { - $defaults['ssl']['capath'] = $result; - } elseif ($result) { - $defaults['ssl']['cafile'] = $result; - } } + + $defaults['ssl']['cafile'] = $targetPath; + } elseif (is_dir($result)) { + $defaults['ssl']['capath'] = $result; } else { - throw new TransportException('A valid cafile or capath could not be located automatically.'); + $defaults['ssl']['cafile'] = $result; } } From c232566e5266366ca3d26030056e86c68356bc73 Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Thu, 21 Jan 2016 16:02:44 +0100 Subject: [PATCH 20/98] add a hash to make sure CA file gets recreated if the content changes --- src/Composer/Util/RemoteFilesystem.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 58e95d53e..a13f6f449 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -639,7 +639,8 @@ class RemoteFilesystem } if (preg_match('{^phar://}', $result)) { - $targetPath = rtrim(sys_get_temp_dir(), '\\/') . '/composer-cacert.pem'; + $hash = md5(file_get_contents($result)); + $targetPath = rtrim(sys_get_temp_dir(), '\\/') . '/composer-cacert-' . $hash . '.pem'; if (!file_exists($targetPath)) { // use stream_copy_to_stream instead of copy From 34f1fcbdcb7316b356e41a6dc71dbd016c6a0b3f Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 22 Jan 2016 01:47:05 +0000 Subject: [PATCH 21/98] Drop downgrade warning --- src/Composer/Util/RemoteFilesystem.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 6ecc349db..e420fa3b7 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -327,16 +327,6 @@ class RemoteFilesystem if (!empty($targetUrl)) { $this->redirects++; - // TODO: Disabled because this is (probably) different behaviour to PHP following for us. - // if ('http' === parse_url($targetUrl, PHP_URL_SCHEME) && 'https' === $this->scheme) { - // // Do not allow protocol downgrade. - // // TODO: Currently this will fail if a request goes http -> https -> http - // $e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded, not permitting protocol downgrade'); - // $e->setHeaders($http_response_header); - // $e->setResponse($result); - // throw $e; - // } - if ($this->io->isDebug()) { $this->io->writeError(sprintf('Following redirect (%u)', $this->redirects)); } From 33f823146b52d5fe9b3826a1e7fad1e10f2037cf Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 22 Jan 2016 01:48:16 +0000 Subject: [PATCH 22/98] Account for ports in URL --- src/Composer/Util/RemoteFilesystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index e420fa3b7..adba6c46e 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -316,7 +316,7 @@ class RemoteFilesystem $urlHost = parse_url($this->fileUrl, PHP_URL_HOST); // Replace path using hostname as an anchor. - $targetUrl = preg_replace('{^(.+(?://|@)'.preg_quote($urlHost).')(?:[/\?].*)?$}', '\1'.$locationHeader, $this->fileUrl); + $targetUrl = preg_replace('{^(.+(?://|@)'.preg_quote($urlHost).'(?::\d+)?)(?:[/\?].*)?$}', '\1'.$locationHeader, $this->fileUrl); } else { // Relative path; e.g. foo // This actually differs from PHP which seems to add duplicate slashes. From 474541e9aa47fa35a99037e73b727eec2ef5be75 Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Fri, 22 Jan 2016 09:14:37 +0100 Subject: [PATCH 23/98] apply comments - add capath to json schema - simplify factory - hash_file and sha256 for CA checking - remove exception as scenario should not occur - remove executable bit from CA file - make CA file also group/world writable (we overwrite invalid content anyway) to avoid permission errors as much as possible --- res/composer-schema.json | 4 + src/Composer/Factory.php | 10 +-- src/Composer/Util/RemoteFilesystem.php | 115 +++++++++++++++---------- 3 files changed, 74 insertions(+), 55 deletions(-) diff --git a/res/composer-schema.json b/res/composer-schema.json index 966b19b86..9d36b6721 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -149,6 +149,10 @@ "type": "string", "description": "A way to set the path to the openssl CA file. In PHP 5.6+ you should rather set this via openssl.cafile in php.ini, although PHP 5.6+ should be able to detect your system CA file automatically." }, + "capath": { + "type": "string", + "description": "If cafile is not specified or if the certificate is not found there, the directory pointed to by capath is searched for a suitable certificate. capath must be a correctly hashed certificate directory." + }, "http-basic": { "type": "object", "description": "A hash of domain name => {\"username\": \"...\", \"password\": \"...\"}.", diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 042445a2a..7998c8be6 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -590,16 +590,10 @@ class Factory $remoteFilesystemOptions = array(); if ($disableTls === false) { if ($config && $config->get('cafile')) { - $remoteFilesystemOptions = array_merge_recursive( - $remoteFilesystemOptions, - array('ssl' => array('cafile' => $config->get('cafile'))) - ); + $remoteFilesystemOptions['ssl']['cafile'] = $config->get('cafile'); } if ($config && $config->get('capath')) { - $remoteFilesystemOptions = array_merge_recursive( - $remoteFilesystemOptions, - array('ssl' => array('capath' => $config->get('capath'))) - ); + $remoteFilesystemOptions['ssl']['capath'] = $config->get('capath'); } } try { diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index a13f6f449..cee448715 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -567,6 +567,11 @@ class RemoteFilesystem return $options; } + /** + * @param array $options + * + * @return array + */ private function getTlsDefaults(array $options) { $ciphers = implode(':', array( @@ -631,27 +636,16 @@ class RemoteFilesystem * Attempt to find a local cafile or throw an exception if none pre-set * The user may go download one if this occurs. */ - if (!isset($defaults['ssl']['cafile'], $defaults['ssl']['capath'])) { + if (!isset($defaults['ssl']['cafile']) && !isset($defaults['ssl']['capath'])) { $result = $this->getSystemCaRootBundlePath(); - if (!$result) { - throw new TransportException('A valid cafile or capath could not be located automatically.'); - } - if (preg_match('{^phar://}', $result)) { - $hash = md5(file_get_contents($result)); + $hash = hash_file('sha256', $result); $targetPath = rtrim(sys_get_temp_dir(), '\\/') . '/composer-cacert-' . $hash . '.pem'; - if (!file_exists($targetPath)) { - // use stream_copy_to_stream instead of copy - // to work around https://bugs.php.net/bug.php?id=64634 - $source = fopen($result, 'r'); - $target = fopen($targetPath, 'w+'); - stream_copy_to_stream($source, $target); - fclose($source); - fclose($target); - chmod($targetPath, 0744); - unset($source, $target); + if (!file_exists($targetPath) || $hash !== hash_file('sha256', $targetPath)) { + $this->safeCopy($result, $targetPath); + chmod($targetPath, 0644); } $defaults['ssl']['cafile'] = $targetPath; @@ -681,37 +675,39 @@ class RemoteFilesystem } /** - * This method was adapted from Sslurp. - * https://github.com/EvanDotPro/Sslurp - * - * (c) Evan Coury - * - * For the full copyright and license information, please see below: - * - * Copyright (c) 2013, Evan Coury - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ + * This method was adapted from Sslurp. + * https://github.com/EvanDotPro/Sslurp + * + * (c) Evan Coury + * + * For the full copyright and license information, please see below: + * + * Copyright (c) 2013, Evan Coury + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @return string + */ private function getSystemCaRootBundlePath() { static $caPath = null; @@ -762,6 +758,11 @@ class RemoteFilesystem return $caPath = __DIR__.'/../../../res/cacert.pem'; // Bundled with Composer, last resort } + /** + * @param string $filename + * + * @return bool + */ private function validateCaFile($filename) { if ($this->io->isDebug()) { @@ -781,4 +782,24 @@ class RemoteFilesystem return (bool) openssl_x509_parse($contents); } + + /** + * Safely copy a file. + * + * Uses stream_copy_to_stream instead of copy to work around https://bugs.php.net/bug.php?id=64634 + * + * @param string $source + * @param string $target + */ + private function safeCopy($source, $target) + { + $source = fopen($source, 'r'); + $target = fopen($target, 'w+'); + + stream_copy_to_stream($source, $target); + fclose($source); + fclose($target); + + unset($source, $target); + } } From 2393222826b7014352e455a2ef24c3e8adadbdc1 Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Fri, 22 Jan 2016 09:20:43 +0100 Subject: [PATCH 24/98] more appropriate name --- src/Composer/Util/RemoteFilesystem.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index cee448715..f501d52b3 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -644,8 +644,8 @@ class RemoteFilesystem $targetPath = rtrim(sys_get_temp_dir(), '\\/') . '/composer-cacert-' . $hash . '.pem'; if (!file_exists($targetPath) || $hash !== hash_file('sha256', $targetPath)) { - $this->safeCopy($result, $targetPath); - chmod($targetPath, 0644); + $this->streamCopy($result, $targetPath); + chmod($targetPath, 0666); } $defaults['ssl']['cafile'] = $targetPath; @@ -784,14 +784,12 @@ class RemoteFilesystem } /** - * Safely copy a file. - * * Uses stream_copy_to_stream instead of copy to work around https://bugs.php.net/bug.php?id=64634 * * @param string $source * @param string $target */ - private function safeCopy($source, $target) + private function streamCopy($source, $target) { $source = fopen($source, 'r'); $target = fopen($target, 'w+'); From d6be2a693b6c35e2294d839335f1908b4339df12 Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Fri, 22 Jan 2016 14:27:08 +0100 Subject: [PATCH 25/98] switch to array-replace-recursive --- src/Composer/Util/RemoteFilesystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index f501d52b3..9a8a25d81 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -629,7 +629,7 @@ class RemoteFilesystem ); if (isset($options['ssl'])) { - $defaults['ssl'] = array_merge_recursive($defaults['ssl'], $options['ssl']); + $defaults['ssl'] = array_replace_recursive($defaults['ssl'], $options['ssl']); } /** From 5b85ee409cba29f1c70f2cff6115eb5ec265a1c9 Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Fri, 22 Jan 2016 14:29:29 +0100 Subject: [PATCH 26/98] add missing array-replace-recursive --- src/Composer/Factory.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 7998c8be6..e6718278f 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -595,6 +595,7 @@ class Factory if ($config && $config->get('capath')) { $remoteFilesystemOptions['ssl']['capath'] = $config->get('capath'); } + $remoteFilesystemOptions = array_replace_recursive($remoteFilesystemOptions, $options); } try { $remoteFilesystem = new RemoteFilesystem($io, $config, $remoteFilesystemOptions, $disableTls); From 2051d74774d7173e28aa7580eeb18ae0dcd4aba8 Mon Sep 17 00:00:00 2001 From: nevvermind Date: Tue, 9 Jun 2015 11:33:57 +0100 Subject: [PATCH 27/98] Added Capable plugins for a more future-proof Plugin API Plugins can now present their capabilities to the PluginManager, through which it can act accordingly, thus making Plugin API more flexible, BC-friendly and decoupled. --- src/Composer/Plugin/Capability/Capability.php | 24 +++++++ src/Composer/Plugin/Capable.php | 48 ++++++++++++++ src/Composer/Plugin/PluginInterface.php | 6 +- src/Composer/Plugin/PluginManager.php | 64 ++++++++++++++++--- .../Composer/Test/Plugin/Mock/Capability.php | 23 +++++++ .../Plugin/Mock/CapablePluginInterface.php | 20 ++++++ .../Test/Plugin/PluginInstallerTest.php | 63 ++++++++++++++++++ 7 files changed, 235 insertions(+), 13 deletions(-) create mode 100644 src/Composer/Plugin/Capability/Capability.php create mode 100644 src/Composer/Plugin/Capable.php create mode 100644 tests/Composer/Test/Plugin/Mock/Capability.php create mode 100644 tests/Composer/Test/Plugin/Mock/CapablePluginInterface.php diff --git a/src/Composer/Plugin/Capability/Capability.php b/src/Composer/Plugin/Capability/Capability.php new file mode 100644 index 000000000..335d7344c --- /dev/null +++ b/src/Composer/Plugin/Capability/Capability.php @@ -0,0 +1,24 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Plugin\Capability; + +/** + * Marker interface for Plugin capabilities. + * Every new Capability which is added to the Plugin API must implement this interface. + * + * @api + * @since Plugin API 1.1 + */ +interface Capability +{ +} diff --git a/src/Composer/Plugin/Capable.php b/src/Composer/Plugin/Capable.php new file mode 100644 index 000000000..1dc8f5bad --- /dev/null +++ b/src/Composer/Plugin/Capable.php @@ -0,0 +1,48 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Plugin; + +/** + * Plugins which need to expose various implementations + * of the Composer Plugin Capabilities must have their + * declared Plugin class implementing this interface. + * + * @api + * @since Plugin API 1.1 + */ +interface Capable +{ + /** + * Method by which a Plugin announces its API implementations, through an array + * with a special structure. + * + * The key must be a string, representing a fully qualified class/interface name + * which Composer Plugin API exposes - named "API class". + * The value must be a string as well, representing the fully qualified class name + * of the API class - named "SPI class". + * + * Every SPI must implement their API class. + * + * Every SPI will be passed a single array parameter via their constructor. + * + * Example: + * // API as key, SPI as value + * return array( + * 'Composer\Plugin\Capability\CommandProvider' => 'My\CommandProvider', + * 'Composer\Plugin\Capability\Validator' => 'My\Validator', + * ); + * + * @return string[] + */ + public function getCapabilities(); +} diff --git a/src/Composer/Plugin/PluginInterface.php b/src/Composer/Plugin/PluginInterface.php index dea5828c1..6eaca4e90 100644 --- a/src/Composer/Plugin/PluginInterface.php +++ b/src/Composer/Plugin/PluginInterface.php @@ -23,14 +23,14 @@ use Composer\IO\IOInterface; interface PluginInterface { /** - * Version number of the fake composer-plugin-api package + * Version number of the internal composer-plugin-api package * * @var string */ - const PLUGIN_API_VERSION = '1.0.0'; + const PLUGIN_API_VERSION = '1.1.0'; /** - * Apply plugin modifications to composer + * Apply plugin modifications to Composer * * @param Composer $composer * @param IOInterface $io diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index 27680907d..a402b0b70 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -23,6 +23,7 @@ use Composer\Package\PackageInterface; use Composer\Package\Link; use Composer\Semver\Constraint\Constraint; use Composer\DependencyResolver\Pool; +use Composer\Plugin\Capability\Capability; /** * Plugin manager @@ -185,16 +186,6 @@ class PluginManager } } - /** - * Returns the version of the internal composer-plugin-api package. - * - * @return string - */ - protected function getPluginApiVersion() - { - return PluginInterface::PLUGIN_API_VERSION; - } - /** * Adds a plugin, activates it and registers it with the event dispatcher * @@ -299,4 +290,57 @@ class PluginManager return $this->globalComposer->getInstallationManager()->getInstallPath($package); } + + /** + * Returns the version of the internal composer-plugin-api package. + * + * @return string + */ + protected function getPluginApiVersion() + { + return PluginInterface::PLUGIN_API_VERSION; + } + + /** + * @param PluginInterface $plugin + * @param string $capability + * @return bool|string The fully qualified class of the implementation or false if none was provided + */ + protected function getCapabilityImplementationClassName(PluginInterface $plugin, $capability) + { + if (!($plugin instanceof Capable)) { + return false; + } + + $capabilities = (array) $plugin->getCapabilities(); + + if (empty($capabilities[$capability]) || !is_string($capabilities[$capability])) { + return false; + } + + return trim($capabilities[$capability]); + } + + /** + * @param PluginInterface $plugin + * @param string $capability The fully qualified name of the API interface which the plugin may provide + * an implementation. + * @param array $ctorArgs Arguments passed to Capability's constructor. + * Keeping it an array will allow future values to be passed w\o changing the signature. + * @return Capability|boolean Bool false if the Plugin has no implementation of the requested Capability. + */ + public function getPluginCapability(PluginInterface $plugin, $capability, array $ctorArgs = array()) + { + if ($capabilityClass = $this->getCapabilityImplementationClassName($plugin, $capability)) { + if (class_exists($capabilityClass)) { + $capabilityObj = new $capabilityClass($ctorArgs); + if ($capabilityObj instanceof Capability && + $capabilityObj instanceof $capability + ) { + return $capabilityObj; + } + } + } + return false; + } } diff --git a/tests/Composer/Test/Plugin/Mock/Capability.php b/tests/Composer/Test/Plugin/Mock/Capability.php new file mode 100644 index 000000000..79635a314 --- /dev/null +++ b/tests/Composer/Test/Plugin/Mock/Capability.php @@ -0,0 +1,23 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Plugin\Mock; + +class Capability implements \Composer\Plugin\Capability\Capability +{ + public $args; + + public function __construct(array $args) + { + $this->args = $args; + } +} diff --git a/tests/Composer/Test/Plugin/Mock/CapablePluginInterface.php b/tests/Composer/Test/Plugin/Mock/CapablePluginInterface.php new file mode 100644 index 000000000..5e8d88c31 --- /dev/null +++ b/tests/Composer/Test/Plugin/Mock/CapablePluginInterface.php @@ -0,0 +1,20 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Plugin\Mock; + +use Composer\Plugin\Capable; +use Composer\Plugin\PluginInterface; + +interface CapablePluginInterface extends PluginInterface, Capable +{ +} diff --git a/tests/Composer/Test/Plugin/PluginInstallerTest.php b/tests/Composer/Test/Plugin/PluginInstallerTest.php index b449d7e90..024561bd2 100644 --- a/tests/Composer/Test/Plugin/PluginInstallerTest.php +++ b/tests/Composer/Test/Plugin/PluginInstallerTest.php @@ -314,4 +314,67 @@ class PluginInstallerTest extends TestCase $this->setPluginApiVersionWithPlugins('5.5.0', $pluginWithApiConstraint); $this->assertCount(0, $this->pm->getPlugins()); } + + public function testIncapablePluginIsCorrectlyDetected() + { + $plugin = $this->getMockBuilder('Composer\Plugin\PluginInterface') + ->getMock(); + + $this->assertFalse($this->pm->getPluginCapability($plugin, 'Fake\Ability')); + } + + public function testCapabilityImplementsComposerPluginApiClassAndIsConstructedWithArgs() + { + $capabilityApi = 'Composer\Plugin\Capability\Capability'; + $capabilitySpi = 'Composer\Test\Plugin\Mock\Capability'; + + $plugin = $this->getMockBuilder('Composer\Test\Plugin\Mock\CapablePluginInterface') + ->getMock(); + + $plugin->expects($this->once()) + ->method('getCapabilities') + ->will($this->returnCallback(function() use ($capabilitySpi, $capabilityApi) { + return array($capabilityApi => $capabilitySpi); + })); + + $capability = $this->pm->getPluginCapability($plugin, $capabilityApi, array('a' => 1, 'b' => 2)); + + $this->assertInstanceOf($capabilityApi, $capability); + $this->assertInstanceOf($capabilitySpi, $capability); + $this->assertSame(array('a' => 1, 'b' => 2), $capability->args); + } + + public function invalidSpiValues() + { + return array( + array(null), + array(""), + array(0), + array(1000), + array(" "), + array(array(1)), + array(array()), + array(new \stdClass()), + array("NonExistentClassLikeMiddleClass"), + ); + } + + /** + * @dataProvider invalidSpiValues + */ + public function testInvalidCapabilitySpiDeclarationsAreDisregarded($invalidSpi) + { + $capabilityApi = 'Composer\Plugin\Capability\Capability'; + + $plugin = $this->getMockBuilder('Composer\Test\Plugin\Mock\CapablePluginInterface') + ->getMock(); + + $plugin->expects($this->once()) + ->method('getCapabilities') + ->will($this->returnCallback(function() use ($invalidSpi, $capabilityApi) { + return array($capabilityApi => $invalidSpi); + })); + + $this->assertFalse($this->pm->getPluginCapability($plugin, $capabilityApi)); + } } From 58ded13eb9a80483a327d3b30ccecd2a9f7c97a0 Mon Sep 17 00:00:00 2001 From: nevvermind Date: Tue, 9 Jun 2015 15:27:11 +0100 Subject: [PATCH 28/98] Fix tests breaking on a api version bump Make generic plugins work with many API versions as opposed to just 1.0.0. --- .../Plugin/Fixtures/plugin-v1/composer.json | 2 +- .../Plugin/Fixtures/plugin-v2/composer.json | 2 +- .../Plugin/Fixtures/plugin-v3/composer.json | 2 +- .../Test/Plugin/PluginInstallerTest.php | 18 ------------------ 4 files changed, 3 insertions(+), 21 deletions(-) diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json index efc552956..ebe56e425 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json @@ -7,6 +7,6 @@ "class": "Installer\\Plugin" }, "require": { - "composer-plugin-api": "1.0.0" + "composer-plugin-api": "*" } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json index 6947ddd5c..7a011070d 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json @@ -7,6 +7,6 @@ "class": "Installer\\Plugin2" }, "require": { - "composer-plugin-api": "1.0.0" + "composer-plugin-api": "*" } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json index 5cb01d019..2061a07e3 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json @@ -7,6 +7,6 @@ "class": "Installer\\Plugin2" }, "require": { - "composer-plugin-api": "1.0.0" + "composer-plugin-api": "*" } } diff --git a/tests/Composer/Test/Plugin/PluginInstallerTest.php b/tests/Composer/Test/Plugin/PluginInstallerTest.php index 024561bd2..32602cd03 100644 --- a/tests/Composer/Test/Plugin/PluginInstallerTest.php +++ b/tests/Composer/Test/Plugin/PluginInstallerTest.php @@ -249,24 +249,6 @@ class PluginInstallerTest extends TestCase $this->pm->loadInstalledPlugins(); } - public function testExactPluginVersionStyleAreRegisteredCorrectly() - { - $pluginsWithFixedAPIVersions = array( - $this->packages[0], - $this->packages[1], - $this->packages[2], - ); - - $this->setPluginApiVersionWithPlugins('1.0.0', $pluginsWithFixedAPIVersions); - $this->assertCount(3, $this->pm->getPlugins()); - - $this->setPluginApiVersionWithPlugins('1.0.1', $pluginsWithFixedAPIVersions); - $this->assertCount(0, $this->pm->getPlugins()); - - $this->setPluginApiVersionWithPlugins('2.0.0-dev', $pluginsWithFixedAPIVersions); - $this->assertCount(0, $this->pm->getPlugins()); - } - public function testStarPluginVersionWorksWithAnyAPIVersion() { $starVersionPlugin = array($this->packages[4]); From 681043355f4d7d6a9a2d4885760a689291711a87 Mon Sep 17 00:00:00 2001 From: nevvermind Date: Mon, 18 Jan 2016 19:14:23 +0000 Subject: [PATCH 29/98] Update test fixtures + fix test --- tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json | 2 +- tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json | 2 +- tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json | 2 +- tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json | 2 +- tests/Composer/Test/Plugin/PluginInstallerTest.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json index ebe56e425..574c4402f 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v1/composer.json @@ -7,6 +7,6 @@ "class": "Installer\\Plugin" }, "require": { - "composer-plugin-api": "*" + "composer-plugin-api": "^1.0" } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json index 7a011070d..27432acfa 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v2/composer.json @@ -7,6 +7,6 @@ "class": "Installer\\Plugin2" }, "require": { - "composer-plugin-api": "*" + "composer-plugin-api": "^1.0" } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json index 2061a07e3..881eb5cae 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v3/composer.json @@ -7,6 +7,6 @@ "class": "Installer\\Plugin2" }, "require": { - "composer-plugin-api": "*" + "composer-plugin-api": "^1.0" } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json b/tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json index 982d34c7b..f61cb3fbd 100644 --- a/tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v4/composer.json @@ -10,6 +10,6 @@ ] }, "require": { - "composer-plugin-api": "1.0.0" + "composer-plugin-api": "^1.0" } } diff --git a/tests/Composer/Test/Plugin/PluginInstallerTest.php b/tests/Composer/Test/Plugin/PluginInstallerTest.php index 32602cd03..a01a018d9 100644 --- a/tests/Composer/Test/Plugin/PluginInstallerTest.php +++ b/tests/Composer/Test/Plugin/PluginInstallerTest.php @@ -147,7 +147,7 @@ class PluginInstallerTest extends TestCase $this->repository ->expects($this->exactly(2)) ->method('getPackages') - ->will($this->returnValue(array())); + ->will($this->returnValue(array($this->packages[3]))); $installer = new PluginInstaller($this->io, $this->composer); $this->pm->loadInstalledPlugins(); From ec8229ffa3cc218e7b2a86ad8b1306146149dc1a Mon Sep 17 00:00:00 2001 From: nevvermind Date: Mon, 18 Jan 2016 19:18:58 +0000 Subject: [PATCH 30/98] Remove @since --- src/Composer/Plugin/Capability/Capability.php | 1 - src/Composer/Plugin/Capable.php | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Composer/Plugin/Capability/Capability.php b/src/Composer/Plugin/Capability/Capability.php index 335d7344c..b12410608 100644 --- a/src/Composer/Plugin/Capability/Capability.php +++ b/src/Composer/Plugin/Capability/Capability.php @@ -17,7 +17,6 @@ namespace Composer\Plugin\Capability; * Every new Capability which is added to the Plugin API must implement this interface. * * @api - * @since Plugin API 1.1 */ interface Capability { diff --git a/src/Composer/Plugin/Capable.php b/src/Composer/Plugin/Capable.php index 1dc8f5bad..bcc4c0934 100644 --- a/src/Composer/Plugin/Capable.php +++ b/src/Composer/Plugin/Capable.php @@ -18,7 +18,6 @@ namespace Composer\Plugin; * declared Plugin class implementing this interface. * * @api - * @since Plugin API 1.1 */ interface Capable { From aa45a482837a98612a9126dac45a46e556ce6f27 Mon Sep 17 00:00:00 2001 From: nevvermind Date: Fri, 22 Jan 2016 13:48:29 +0000 Subject: [PATCH 31/98] Refactoring - changed "SPI" into something more familiar, like "implementation" - throw exceptions on invalid implementation types or invalid class names - use null instead of false when querying - refactored the tests accordingly --- src/Composer/Plugin/Capable.php | 14 ++--- src/Composer/Plugin/PluginManager.php | 56 +++++++++++-------- .../Test/Plugin/PluginInstallerTest.php | 41 ++++++++++---- 3 files changed, 69 insertions(+), 42 deletions(-) diff --git a/src/Composer/Plugin/Capable.php b/src/Composer/Plugin/Capable.php index bcc4c0934..025e311b3 100644 --- a/src/Composer/Plugin/Capable.php +++ b/src/Composer/Plugin/Capable.php @@ -26,19 +26,17 @@ interface Capable * with a special structure. * * The key must be a string, representing a fully qualified class/interface name - * which Composer Plugin API exposes - named "API class". + * which Composer Plugin API exposes. * The value must be a string as well, representing the fully qualified class name - * of the API class - named "SPI class". + * of the API class. * - * Every SPI must implement their API class. + * Every implementation will be passed a single array parameter via their constructor. * - * Every SPI will be passed a single array parameter via their constructor. + * @tutorial * - * Example: - * // API as key, SPI as value * return array( - * 'Composer\Plugin\Capability\CommandProvider' => 'My\CommandProvider', - * 'Composer\Plugin\Capability\Validator' => 'My\Validator', + * 'Composer\Plugin\Capability\CommandProvider' => 'My\CommandProvider', + * 'Composer\Plugin\Capability\Validator' => 'My\Validator', * ); * * @return string[] diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index a402b0b70..a95b34aa7 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -186,6 +186,16 @@ class PluginManager } } + /** + * Returns the version of the internal composer-plugin-api package. + * + * @return string + */ + protected function getPluginApiVersion() + { + return PluginInterface::PLUGIN_API_VERSION; + } + /** * Adds a plugin, activates it and registers it with the event dispatcher * @@ -291,56 +301,58 @@ class PluginManager return $this->globalComposer->getInstallationManager()->getInstallPath($package); } - /** - * Returns the version of the internal composer-plugin-api package. - * - * @return string - */ - protected function getPluginApiVersion() - { - return PluginInterface::PLUGIN_API_VERSION; - } - /** * @param PluginInterface $plugin * @param string $capability - * @return bool|string The fully qualified class of the implementation or false if none was provided + * @return null|string The fully qualified class of the implementation or null if Plugin is not of Capable type + * @throws \RuntimeException On empty or non-string implementation class name value */ protected function getCapabilityImplementationClassName(PluginInterface $plugin, $capability) { if (!($plugin instanceof Capable)) { - return false; + return null; } $capabilities = (array) $plugin->getCapabilities(); + if (!empty($capabilities[$capability]) && is_string($capabilities[$capability])) { + $capabilities[$capability] = trim($capabilities[$capability]); + } + if (empty($capabilities[$capability]) || !is_string($capabilities[$capability])) { - return false; + throw new \RuntimeException('Plugin provided invalid capability class name(s)'); } - return trim($capabilities[$capability]); + return $capabilities[$capability]; } /** * @param PluginInterface $plugin - * @param string $capability The fully qualified name of the API interface which the plugin may provide + * @param string $capabilityClassName The fully qualified name of the API interface which the plugin may provide * an implementation. - * @param array $ctorArgs Arguments passed to Capability's constructor. - * Keeping it an array will allow future values to be passed w\o changing the signature. - * @return Capability|boolean Bool false if the Plugin has no implementation of the requested Capability. + * @param array $ctorArgs Arguments passed to Capability's constructor. + * Keeping it an array will allow future values to be passed w\o changing the signature. + * @return null|Capability */ - public function getPluginCapability(PluginInterface $plugin, $capability, array $ctorArgs = array()) + public function getPluginCapability(PluginInterface $plugin, $capabilityClassName, array $ctorArgs = array()) { - if ($capabilityClass = $this->getCapabilityImplementationClassName($plugin, $capability)) { + if ($capabilityClass = $this->getCapabilityImplementationClassName($plugin, $capabilityClassName)) { if (class_exists($capabilityClass)) { $capabilityObj = new $capabilityClass($ctorArgs); if ($capabilityObj instanceof Capability && - $capabilityObj instanceof $capability + $capabilityObj instanceof $capabilityClassName ) { return $capabilityObj; + } else { + throw new \RuntimeException( + 'Class ' . $capabilityClass . ' must be of both \Composer\Plugin\Capability\Capability and '. + $capabilityClassName . ' types.' + ); } + } else { + throw new \RuntimeException("Cannot instantiate Capability, as class $capabilityClass does not exist."); } } - return false; + return null; } } diff --git a/tests/Composer/Test/Plugin/PluginInstallerTest.php b/tests/Composer/Test/Plugin/PluginInstallerTest.php index a01a018d9..932730d8a 100644 --- a/tests/Composer/Test/Plugin/PluginInstallerTest.php +++ b/tests/Composer/Test/Plugin/PluginInstallerTest.php @@ -302,31 +302,31 @@ class PluginInstallerTest extends TestCase $plugin = $this->getMockBuilder('Composer\Plugin\PluginInterface') ->getMock(); - $this->assertFalse($this->pm->getPluginCapability($plugin, 'Fake\Ability')); + $this->assertNull($this->pm->getPluginCapability($plugin, 'Fake\Ability')); } public function testCapabilityImplementsComposerPluginApiClassAndIsConstructedWithArgs() { $capabilityApi = 'Composer\Plugin\Capability\Capability'; - $capabilitySpi = 'Composer\Test\Plugin\Mock\Capability'; + $capabilityImplementation = 'Composer\Test\Plugin\Mock\Capability'; $plugin = $this->getMockBuilder('Composer\Test\Plugin\Mock\CapablePluginInterface') ->getMock(); $plugin->expects($this->once()) ->method('getCapabilities') - ->will($this->returnCallback(function() use ($capabilitySpi, $capabilityApi) { - return array($capabilityApi => $capabilitySpi); + ->will($this->returnCallback(function() use ($capabilityImplementation, $capabilityApi) { + return array($capabilityApi => $capabilityImplementation); })); $capability = $this->pm->getPluginCapability($plugin, $capabilityApi, array('a' => 1, 'b' => 2)); $this->assertInstanceOf($capabilityApi, $capability); - $this->assertInstanceOf($capabilitySpi, $capability); + $this->assertInstanceOf($capabilityImplementation, $capability); $this->assertSame(array('a' => 1, 'b' => 2), $capability->args); } - public function invalidSpiValues() + public function invalidImplementationClassNames() { return array( array(null), @@ -337,14 +337,22 @@ class PluginInstallerTest extends TestCase array(array(1)), array(array()), array(new \stdClass()), - array("NonExistentClassLikeMiddleClass"), + ); + } + + public function nonExistingOrInvalidImplementationClassTypes() + { + return array( + array('\stdClass'), + array('NonExistentClassLikeMiddleClass'), ); } /** - * @dataProvider invalidSpiValues + * @dataProvider invalidImplementationClassNames + * @expectedException \RuntimeException */ - public function testInvalidCapabilitySpiDeclarationsAreDisregarded($invalidSpi) + public function testQueryingWithInvalidCapabilityClassNameThrows($invalidImplementationClassNames) { $capabilityApi = 'Composer\Plugin\Capability\Capability'; @@ -353,10 +361,19 @@ class PluginInstallerTest extends TestCase $plugin->expects($this->once()) ->method('getCapabilities') - ->will($this->returnCallback(function() use ($invalidSpi, $capabilityApi) { - return array($capabilityApi => $invalidSpi); + ->will($this->returnCallback(function() use ($invalidImplementationClassNames, $capabilityApi) { + return array($capabilityApi => $invalidImplementationClassNames); })); - $this->assertFalse($this->pm->getPluginCapability($plugin, $capabilityApi)); + $this->pm->getPluginCapability($plugin, $capabilityApi); + } + + /** + * @dataProvider nonExistingOrInvalidImplementationClassTypes + * @expectedException \RuntimeException + */ + public function testQueryingWithNonExistingOrWrongCapabilityClassTypesThrows($wrongImplementationClassTypes) + { + $this->testQueryingWithInvalidCapabilityClassNameThrows($wrongImplementationClassTypes); } } From 5ec6988218b7745c28e08a8c13cb9d920ad3ac01 Mon Sep 17 00:00:00 2001 From: nevvermind Date: Fri, 22 Jan 2016 13:54:59 +0000 Subject: [PATCH 32/98] Fixed docs and removed implementation detail --- src/Composer/Plugin/Capable.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Composer/Plugin/Capable.php b/src/Composer/Plugin/Capable.php index 025e311b3..48ae42250 100644 --- a/src/Composer/Plugin/Capable.php +++ b/src/Composer/Plugin/Capable.php @@ -28,9 +28,7 @@ interface Capable * The key must be a string, representing a fully qualified class/interface name * which Composer Plugin API exposes. * The value must be a string as well, representing the fully qualified class name - * of the API class. - * - * Every implementation will be passed a single array parameter via their constructor. + * of the implementing class. * * @tutorial * From ddd140fd1cceece4841e2f0182d0c5e4fed5c10e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 22 Jan 2016 18:50:30 +0000 Subject: [PATCH 33/98] Rollback plugin api version to 1.0.0 for now, add warning about requiring 1.0.0 exactly --- src/Composer/Plugin/PluginInterface.php | 2 +- src/Composer/Plugin/PluginManager.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Composer/Plugin/PluginInterface.php b/src/Composer/Plugin/PluginInterface.php index 6eaca4e90..a8c2b6a94 100644 --- a/src/Composer/Plugin/PluginInterface.php +++ b/src/Composer/Plugin/PluginInterface.php @@ -27,7 +27,7 @@ interface PluginInterface * * @var string */ - const PLUGIN_API_VERSION = '1.1.0'; + const PLUGIN_API_VERSION = '1.0.0'; /** * Apply plugin modifications to Composer diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index a95b34aa7..844b79b76 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -123,7 +123,9 @@ class PluginManager $currentPluginApiVersion = $this->getPluginApiVersion(); $currentPluginApiConstraint = new Constraint('==', $this->versionParser->normalize($currentPluginApiVersion)); - if (!$requiresComposer->matches($currentPluginApiConstraint)) { + if ($requiresComposer->getPrettyString() === '1.0.0' && $this->getPluginApiVersion() === '1.0.0') { + $this->io->writeError('The "' . $package->getName() . '" plugin requires composer-plugin-api 1.0.0, this *WILL* break in the future and it should be fixed ASAP (require ^1.0 for example).'); + } elseif (!$requiresComposer->matches($currentPluginApiConstraint)) { $this->io->writeError('The "' . $package->getName() . '" plugin was skipped because it requires a Plugin API version ("' . $requiresComposer->getPrettyString() . '") that does not match your Composer installation ("' . $currentPluginApiVersion . '"). You may need to run composer update with the "--no-plugins" option.'); return; } From 837fa805ec9f8dcb1e05e0fca4099f0dab4f1e04 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 22 Jan 2016 19:09:44 +0000 Subject: [PATCH 34/98] Code tweaks, refs #4124 --- src/Composer/Plugin/PluginManager.php | 45 +++++++++---------- .../Test/Plugin/PluginInstallerTest.php | 18 +++++++- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index 844b79b76..49a783579 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -306,7 +306,7 @@ class PluginManager /** * @param PluginInterface $plugin * @param string $capability - * @return null|string The fully qualified class of the implementation or null if Plugin is not of Capable type + * @return null|string The fully qualified class of the implementation or null if Plugin is not of Capable type or does not provide it * @throws \RuntimeException On empty or non-string implementation class name value */ protected function getCapabilityImplementationClassName(PluginInterface $plugin, $capability) @@ -317,21 +317,22 @@ class PluginManager $capabilities = (array) $plugin->getCapabilities(); - if (!empty($capabilities[$capability]) && is_string($capabilities[$capability])) { - $capabilities[$capability] = trim($capabilities[$capability]); + if (!empty($capabilities[$capability]) && is_string($capabilities[$capability]) && trim($capabilities[$capability])) { + return trim($capabilities[$capability]); } - if (empty($capabilities[$capability]) || !is_string($capabilities[$capability])) { - throw new \RuntimeException('Plugin provided invalid capability class name(s)'); + if ( + array_key_exists($capability, $capabilities) + && (empty($capabilities[$capability]) || !is_string($capabilities[$capability]) || !trim($capabilities[$capability])) + ) { + throw new \UnexpectedValueException('Plugin '.get_class($plugin).' provided invalid capability class name(s), got '.var_export($capabilities[$capability], 1)); } - - return $capabilities[$capability]; } /** * @param PluginInterface $plugin * @param string $capabilityClassName The fully qualified name of the API interface which the plugin may provide - * an implementation. + * an implementation of. * @param array $ctorArgs Arguments passed to Capability's constructor. * Keeping it an array will allow future values to be passed w\o changing the signature. * @return null|Capability @@ -339,22 +340,20 @@ class PluginManager public function getPluginCapability(PluginInterface $plugin, $capabilityClassName, array $ctorArgs = array()) { if ($capabilityClass = $this->getCapabilityImplementationClassName($plugin, $capabilityClassName)) { - if (class_exists($capabilityClass)) { - $capabilityObj = new $capabilityClass($ctorArgs); - if ($capabilityObj instanceof Capability && - $capabilityObj instanceof $capabilityClassName - ) { - return $capabilityObj; - } else { - throw new \RuntimeException( - 'Class ' . $capabilityClass . ' must be of both \Composer\Plugin\Capability\Capability and '. - $capabilityClassName . ' types.' - ); - } - } else { - throw new \RuntimeException("Cannot instantiate Capability, as class $capabilityClass does not exist."); + if (!class_exists($capabilityClass)) { + throw new \RuntimeException("Cannot instantiate Capability, as class $capabilityClass from plugin ".get_class($plugin)." does not exist."); + } + + $capabilityObj = new $capabilityClass($ctorArgs); + + // FIXME these could use is_a and do the check *before* instantiating once drop support for php<5.3.9 + if (!$capabilityObj instanceof Capability || !$capabilityObj instanceof $capabilityClassName) { + throw new \RuntimeException( + 'Class ' . $capabilityClass . ' must implement both Composer\Plugin\Capability\Capability and '. $capabilityClassName . '.' + ); } + + return $capabilityObj; } - return null; } } diff --git a/tests/Composer/Test/Plugin/PluginInstallerTest.php b/tests/Composer/Test/Plugin/PluginInstallerTest.php index 932730d8a..c26965d33 100644 --- a/tests/Composer/Test/Plugin/PluginInstallerTest.php +++ b/tests/Composer/Test/Plugin/PluginInstallerTest.php @@ -350,7 +350,7 @@ class PluginInstallerTest extends TestCase /** * @dataProvider invalidImplementationClassNames - * @expectedException \RuntimeException + * @expectedException \UnexpectedValueException */ public function testQueryingWithInvalidCapabilityClassNameThrows($invalidImplementationClassNames) { @@ -368,6 +368,22 @@ class PluginInstallerTest extends TestCase $this->pm->getPluginCapability($plugin, $capabilityApi); } + public function testQueryingNonProvidedCapabilityReturnsNullSafely() + { + $capabilityApi = 'Composer\Plugin\Capability\MadeUpCapability'; + + $plugin = $this->getMockBuilder('Composer\Test\Plugin\Mock\CapablePluginInterface') + ->getMock(); + + $plugin->expects($this->once()) + ->method('getCapabilities') + ->will($this->returnCallback(function() { + return array(); + })); + + $this->assertNull($this->pm->getPluginCapability($plugin, $capabilityApi)); + } + /** * @dataProvider nonExistingOrInvalidImplementationClassTypes * @expectedException \RuntimeException From 05c5aee1f1a453818b29ad58310e4ed4f810c915 Mon Sep 17 00:00:00 2001 From: Omar Shaban Date: Sat, 23 Jan 2016 20:50:43 +0200 Subject: [PATCH 35/98] Fix Broken Links in troubleshooting.md --- doc/articles/troubleshooting.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/articles/troubleshooting.md b/doc/articles/troubleshooting.md index 3edc793ba..f5b37a0ef 100644 --- a/doc/articles/troubleshooting.md +++ b/doc/articles/troubleshooting.md @@ -76,16 +76,16 @@ This is a list of common pitfalls on using Composer, and how to avoid them. ## I have a dependency which contains a "repositories" definition in its composer.json, but it seems to be ignored. -The [`repositories`](04-schema.md#repositories) configuration property is defined as [root-only] -(04-schema.md#root-package). It is not inherited. You can read more about the reasons behind this in the "[why can't -composer load repositories recursively?](articles/why-can't-composer-load-repositories-recursively.md)" article. +The [`repositories`](../04-schema.md#repositories) configuration property is defined as [root-only] +(../04-schema.md#root-package). It is not inherited. You can read more about the reasons behind this in the "[why can't +composer load repositories recursively?](../faqs/why-can't-composer-load-repositories-recursively.md)" article. The simplest work-around to this limitation, is moving or duplicating the `repositories` definition into your root composer.json. ## I have locked a dependency to a specific commit but get unexpected results. While Composer supports locking dependencies to a specific commit using the `#commit-ref` syntax, there are certain -caveats that one should take into account. The most important one is [documented](04-schema.md#package-links), but +caveats that one should take into account. The most important one is [documented](../04-schema.md#package-links), but frequently overlooked: > **Note:** While this is convenient at times, it should not be how you use From 7e2a015e9ba998ced1d7865cd4b5ff1193174ab2 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 19 Jan 2016 23:54:23 +0000 Subject: [PATCH 36/98] Provide support for subjectAltName on PHP < 5.6 --- src/Composer/Util/RemoteFilesystem.php | 157 +++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 9a8a25d81..b3bd6030f 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -33,6 +33,7 @@ class RemoteFilesystem private $progress; private $lastProgress; private $options = array(); + private $peerCertificateMap = array(); private $disableTls = false; private $retryAuthFailure; private $lastHeaders; @@ -252,6 +253,18 @@ class RemoteFilesystem }); try { $result = file_get_contents($fileUrl, false, $ctx); + + if (PHP_VERSION_ID < 50600 && !empty($options['ssl']['peer_fingerprint'])) { + // Emulate fingerprint validation on PHP < 5.6 + $params = stream_context_get_params($ctx); + $expectedPeerFingerprint = $options['ssl']['peer_fingerprint']; + $peerFingerprint = $this->getCertificateFingerprint($params['options']['ssl']['peer_certificate']); + + // Constant time compare??! + if ($expectedPeerFingerprint !== $peerFingerprint) { + throw new TransportException('Peer fingerprint did not match'); + } + } } catch (\Exception $e) { if ($e instanceof TransportException && !empty($http_response_header[0])) { $e->setHeaders($http_response_header); @@ -353,6 +366,32 @@ class RemoteFilesystem } } + if (false === $result && false !== strpos($errorMessage, 'Peer certificate') && PHP_VERSION_ID < 50600) { + // Certificate name error, PHP doesn't support subjectAltName on PHP < 5.6 + // The procedure to handle sAN for older PHP's is: + // + // 1. Open socket to remote server and fetch certificate (disabling peer + // validation because PHP errors without giving up the certificate.) + // + // 2. Verifying the domain in the URL against the names in the sAN field. + // If there is a match record the authority [host/port], certificate + // common name, and certificate fingerprint. + // + // 3. Retry the original request but changing the CN_match parameter to + // the common name extracted from the certificate in step 2. + // + // 4. To prevent any attempt at being hoodwinked by switching the + // certificate between steps 2 and 3 the fingerprint of the certificate + // presented in step 3 is compared against the one recorded in step 2. + $certDetails = $this->getCertificateCnAndFp($this->fileUrl, $options); + + if ($certDetails) { + $this->peerCertificateMap[$this->getUrlAuthority($this->fileUrl)] = $certDetails; + + $this->retry = true; + } + } + if ($this->retry) { $this->retry = false; @@ -529,6 +568,14 @@ class RemoteFilesystem $tlsOptions['ssl']['SNI_server_name'] = $host; } + if (isset($this->peerCertificateMap[$this->getUrlAuthority($originUrl)])) { + // Handle subjectAltName on lesser PHP's. + $certMap = $this->peerCertificateMap[$this->getUrlAuthority($originUrl)]; + + $tlsOptions['ssl']['CN_match'] = $certMap['cn']; + $tlsOptions['ssl']['peer_fingerprint'] = $certMap['fp']; + } + $headers = array(); if (extension_loaded('zlib')) { @@ -625,6 +672,7 @@ class RemoteFilesystem 'verify_peer' => true, 'verify_depth' => 7, 'SNI_enabled' => true, + 'capture_peer_cert' => true, ) ); @@ -800,4 +848,113 @@ class RemoteFilesystem unset($source, $target); } + + private function getCertificateCnAndFp($url, $options) + { + $context = StreamContextFactory::getContext($url, $options, array('options' => array( + 'ssl' => array( + 'capture_peer_cert' => true, + 'verify_peer' => false, // Yes this is fucking insane! But PHP is lame. + )) + )); + + if (false === $handle = @fopen($url, 'rb', false, $context)) { + return; + } + + // Close non authenticated connection without reading any content. + fclose($handle); + + $params = stream_context_get_params($context); + + if (!empty($params['options']['ssl']['peer_certificate'])) { + $peerCertificate = $params['options']['ssl']['peer_certificate']; + + $fp = $this->getCertificateFingerprint($peerCertificate); + $cert = openssl_x509_parse($peerCertificate, false); + $commonName = $cert['subject']['commonName']; + + $subjectAltName = preg_split('{\s*,\s*}', $cert['extensions']['subjectAltName']); + $subjectAltName = array_filter(array_map(function ($name) { + if (0 === strpos($name, 'DNS:')) { + return substr($name, 4); + } + }, $subjectAltName)); + + if (in_array(parse_url($url, PHP_URL_HOST), $subjectAltName, true)) { + return array( + 'cn' => $commonName, + 'fp' => $fp, + ); + } + + // TODO: Support wildcards. + } + } + + /** + * Get the certificate pin. + * + * By Kevin McArthur of StormTide Digital Studios Inc. + * @KevinSMcArthur / https://github.com/StormTide + * + * See http://tools.ietf.org/html/draft-ietf-websec-key-pinning-02 + * + * This method was adapted from Sslurp. + * https://github.com/EvanDotPro/Sslurp + * + * (c) Evan Coury + * + * For the full copyright and license information, please see below: + * + * Copyright (c) 2013, Evan Coury + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + private function getCertificateFingerprint($certificate) + { + $pubkeydetails = openssl_pkey_get_details(openssl_get_publickey($certificate)); + $pubkeypem = $pubkeydetails['key']; + //Convert PEM to DER before SHA1'ing + $start = '-----BEGIN PUBLIC KEY-----'; + $end = '-----END PUBLIC KEY-----'; + $pemtrim = substr($pubkeypem, (strpos($pubkeypem, $start) + strlen($start)), (strlen($pubkeypem) - strpos($pubkeypem, $end)) * (-1)); + $der = base64_decode($pemtrim); + + return sha1($der); + } + + private function getUrlAuthority($url) + { + $defaultPorts = array( + 'ftp' => 21, + 'http' => 80, + 'https' => 443, + ); + + $defaultPort = $defaultPorts[parse_url($this->fileUrl, PHP_URL_SCHEME)]; + $port = parse_url($this->fileUrl, PHP_URL_PORT) ?: $defaultPort; + + return parse_url($this->fileUrl, PHP_URL_HOST).':'.$port; + } } From 304c268c3bf6bfee2b5320be25bd43a329fd1192 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sun, 24 Jan 2016 18:52:09 +0000 Subject: [PATCH 37/98] Tidy up and general improvement of sAN handling code * Move OpenSSL functions into a new TlsHelper class * Add error when sAN certificate cannot be verified due to CVE-2013-6420 * Throw exception if PHP >= 5.6 manages to use fallback code * Add support for wildcards in CN/sAN * Add tests for cert name validation * Check for backported security fix for CVE-2013-6420 using testcase from PHP tests. * Whitelist some disto PHP versions that have the CVE-2013-6420 fix backported. --- src/Composer/Util/RemoteFilesystem.php | 138 ++++------ src/Composer/Util/TlsHelper.php | 289 +++++++++++++++++++++ tests/Composer/Test/Util/TlsHelperTest.php | 76 ++++++ 3 files changed, 422 insertions(+), 81 deletions(-) create mode 100644 src/Composer/Util/TlsHelper.php create mode 100644 tests/Composer/Test/Util/TlsHelperTest.php diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index b3bd6030f..cee0670c0 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -258,7 +258,7 @@ class RemoteFilesystem // Emulate fingerprint validation on PHP < 5.6 $params = stream_context_get_params($ctx); $expectedPeerFingerprint = $options['ssl']['peer_fingerprint']; - $peerFingerprint = $this->getCertificateFingerprint($params['options']['ssl']['peer_certificate']); + $peerFingerprint = TlsHelper::getCertificateFingerprint($params['options']['ssl']['peer_certificate']); // Constant time compare??! if ($expectedPeerFingerprint !== $peerFingerprint) { @@ -383,12 +383,19 @@ class RemoteFilesystem // 4. To prevent any attempt at being hoodwinked by switching the // certificate between steps 2 and 3 the fingerprint of the certificate // presented in step 3 is compared against the one recorded in step 2. - $certDetails = $this->getCertificateCnAndFp($this->fileUrl, $options); + if (TlsHelper::isOpensslParseSafe()) { + $certDetails = $this->getCertificateCnAndFp($this->fileUrl, $options); - if ($certDetails) { - $this->peerCertificateMap[$this->getUrlAuthority($this->fileUrl)] = $certDetails; + if ($certDetails) { + $this->peerCertificateMap[$this->getUrlAuthority($this->fileUrl)] = $certDetails; - $this->retry = true; + $this->retry = true; + } + } else { + $this->io->writeError(sprintf( + 'Your version of PHP, %s, is affected by CVE-2013-6420 and cannot safely perform certificate validation, we strongly suggest you upgrade.', + PHP_VERSION + )); } } @@ -566,14 +573,24 @@ class RemoteFilesystem $tlsOptions['ssl']['CN_match'] = $host; $tlsOptions['ssl']['SNI_server_name'] = $host; - } - if (isset($this->peerCertificateMap[$this->getUrlAuthority($originUrl)])) { - // Handle subjectAltName on lesser PHP's. - $certMap = $this->peerCertificateMap[$this->getUrlAuthority($originUrl)]; + $urlAuthority = $this->getUrlAuthority($this->fileUrl); + + if (isset($this->peerCertificateMap[$urlAuthority])) { + // Handle subjectAltName on lesser PHP's. + $certMap = $this->peerCertificateMap[$urlAuthority]; + + if ($this->io->isDebug()) { + $this->io->writeError(sprintf( + 'Using %s as CN for subjectAltName enabled host %s', + $certMap['cn'], + $urlAuthority + )); + } - $tlsOptions['ssl']['CN_match'] = $certMap['cn']; - $tlsOptions['ssl']['peer_fingerprint'] = $certMap['fp']; + $tlsOptions['ssl']['CN_match'] = $certMap['cn']; + $tlsOptions['ssl']['peer_fingerprint'] = $certMap['fp']; + } } $headers = array(); @@ -849,8 +866,20 @@ class RemoteFilesystem unset($source, $target); } + /** + * Fetch certificate common name and fingerprint for validation of SAN. + * + * @todo Remove when PHP 5.6 is minimum supported version. + */ private function getCertificateCnAndFp($url, $options) { + if (PHP_VERSION_ID >= 50600) { + throw new \BadMethodCallException(sprintf( + '%s must not be used on PHP >= 5.6', + __METHOD__ + )); + } + $context = StreamContextFactory::getContext($url, $options, array('options' => array( 'ssl' => array( 'capture_peer_cert' => true, @@ -858,92 +887,30 @@ class RemoteFilesystem )) )); + // Ideally this would just use stream_socket_client() to avoid sending a + // HTTP request but that does not capture the certificate. if (false === $handle = @fopen($url, 'rb', false, $context)) { return; } // Close non authenticated connection without reading any content. fclose($handle); + $handle = null; $params = stream_context_get_params($context); if (!empty($params['options']['ssl']['peer_certificate'])) { $peerCertificate = $params['options']['ssl']['peer_certificate']; - $fp = $this->getCertificateFingerprint($peerCertificate); - $cert = openssl_x509_parse($peerCertificate, false); - $commonName = $cert['subject']['commonName']; - - $subjectAltName = preg_split('{\s*,\s*}', $cert['extensions']['subjectAltName']); - $subjectAltName = array_filter(array_map(function ($name) { - if (0 === strpos($name, 'DNS:')) { - return substr($name, 4); - } - }, $subjectAltName)); - - if (in_array(parse_url($url, PHP_URL_HOST), $subjectAltName, true)) { + if (TlsHelper::checkCertificateHost($peerCertificate, parse_url($url, PHP_URL_HOST), $commonName)) { return array( 'cn' => $commonName, - 'fp' => $fp, + 'fp' => TlsHelper::getCertificateFingerprint($peerCertificate), ); } - - // TODO: Support wildcards. } } - /** - * Get the certificate pin. - * - * By Kevin McArthur of StormTide Digital Studios Inc. - * @KevinSMcArthur / https://github.com/StormTide - * - * See http://tools.ietf.org/html/draft-ietf-websec-key-pinning-02 - * - * This method was adapted from Sslurp. - * https://github.com/EvanDotPro/Sslurp - * - * (c) Evan Coury - * - * For the full copyright and license information, please see below: - * - * Copyright (c) 2013, Evan Coury - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - private function getCertificateFingerprint($certificate) - { - $pubkeydetails = openssl_pkey_get_details(openssl_get_publickey($certificate)); - $pubkeypem = $pubkeydetails['key']; - //Convert PEM to DER before SHA1'ing - $start = '-----BEGIN PUBLIC KEY-----'; - $end = '-----END PUBLIC KEY-----'; - $pemtrim = substr($pubkeypem, (strpos($pubkeypem, $start) + strlen($start)), (strlen($pubkeypem) - strpos($pubkeypem, $end)) * (-1)); - $der = base64_decode($pemtrim); - - return sha1($der); - } - private function getUrlAuthority($url) { $defaultPorts = array( @@ -952,9 +919,18 @@ class RemoteFilesystem 'https' => 443, ); - $defaultPort = $defaultPorts[parse_url($this->fileUrl, PHP_URL_SCHEME)]; - $port = parse_url($this->fileUrl, PHP_URL_PORT) ?: $defaultPort; + $scheme = parse_url($url, PHP_URL_SCHEME); + + if (!isset($defaultPorts[$scheme])) { + throw new \InvalidArgumentException(sprintf( + 'Could not get default port for unknown scheme: %s', + $scheme + )); + } + + $defaultPort = $defaultPorts[$scheme]; + $port = parse_url($url, PHP_URL_PORT) ?: $defaultPort; - return parse_url($this->fileUrl, PHP_URL_HOST).':'.$port; + return parse_url($url, PHP_URL_HOST).':'.$port; } } diff --git a/src/Composer/Util/TlsHelper.php b/src/Composer/Util/TlsHelper.php new file mode 100644 index 000000000..ce6738cdd --- /dev/null +++ b/src/Composer/Util/TlsHelper.php @@ -0,0 +1,289 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +use Symfony\Component\Process\PhpProcess; + +/** + * @author Chris Smith + */ +final class TlsHelper +{ + private static $useOpensslParse; + + /** + * Match hostname against a certificate. + * + * @param mixed $certificate X.509 certificate + * @param string $hostname Hostname in the URL + * @param string $cn Set to the common name of the certificate iff match found + * + * @return bool + */ + public static function checkCertificateHost($certificate, $hostname, &$cn = null) + { + $names = self::getCertificateNames($certificate); + + if (empty($names)) { + return false; + } + + $combinedNames = array_merge($names['san'], array($names['cn'])); + $hostname = strtolower($hostname); + + foreach ($combinedNames as $certName) { + $matcher = self::certNameMatcher($certName); + + if ($matcher && $matcher($hostname)) { + $cn = $names['cn']; + return true; + } + } + + return false; + } + + + /** + * Extract DNS names out of an X.509 certificate. + * + * @param mixed $certificate X.509 certificate + * + * @return array|null + */ + public static function getCertificateNames($certificate) + { + if (is_array($certificate)) { + $info = $certificate; + } elseif (self::isOpensslParseSafe()) { + $info = openssl_x509_parse($certificate, false); + } + + if (!isset($info['subject']['commonName'])) { + return; + } + + $commonName = strtolower($info['subject']['commonName']); + $subjectAltNames = array(); + + if (isset($info['extensions']['subjectAltName'])) { + $subjectAltNames = preg_split('{\s*,\s*}', $info['extensions']['subjectAltName']); + $subjectAltNames = array_filter(array_map(function ($name) { + if (0 === strpos($name, 'DNS:')) { + return strtolower(ltrim(substr($name, 4))); + } + }, $subjectAltNames)); + $subjectAltNames = array_values($subjectAltNames); + } + + return array( + 'cn' => $commonName, + 'san' => $subjectAltNames, + ); + } + + /** + * Get the certificate pin. + * + * By Kevin McArthur of StormTide Digital Studios Inc. + * @KevinSMcArthur / https://github.com/StormTide + * + * See http://tools.ietf.org/html/draft-ietf-websec-key-pinning-02 + * + * This method was adapted from Sslurp. + * https://github.com/EvanDotPro/Sslurp + * + * (c) Evan Coury + * + * For the full copyright and license information, please see below: + * + * Copyright (c) 2013, Evan Coury + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + public static function getCertificateFingerprint($certificate) + { + $pubkeydetails = openssl_pkey_get_details(openssl_get_publickey($certificate)); + $pubkeypem = $pubkeydetails['key']; + //Convert PEM to DER before SHA1'ing + $start = '-----BEGIN PUBLIC KEY-----'; + $end = '-----END PUBLIC KEY-----'; + $pemtrim = substr($pubkeypem, (strpos($pubkeypem, $start) + strlen($start)), (strlen($pubkeypem) - strpos($pubkeypem, $end)) * (-1)); + $der = base64_decode($pemtrim); + + return sha1($der); + } + + /** + * Test if it is safe to use the PHP function openssl_x509_parse(). + * + * This checks if OpenSSL extensions is vulnerable to remote code execution + * via the exploit documented as CVE-2013-6420. + * + * @return bool + */ + public static function isOpensslParseSafe() + { + if (null !== self::$useOpensslParse) { + return self::$useOpensslParse; + } + + if (PHP_VERSION_ID >= 50600) { + return self::$useOpensslParse = true; + } + + // Vulnerable: + // PHP 5.3.0 - PHP 5.3.27 + // PHP 5.4.0 - PHP 5.4.22 + // PHP 5.5.0 - PHP 5.5.6 + if ( + (PHP_VERSION_ID < 50400 && PHP_VERSION_ID >= 50328) + || (PHP_VERSION_ID < 50500 && PHP_VERSION_ID >= 50423) + || (PHP_VERSION_ID < 50600 && PHP_VERSION_ID >= 50507) + ) { + // This version of PHP has the fix for CVE-2013-6420 applied. + return self::$useOpensslParse = true; + } + + if ('\\' === DIRECTORY_SEPARATOR) { + // Windows is probably insecure in this case. + return self::$useOpensslParse = false; + } + + $compareDistroVersionPrefix = function ($prefix, $fixedVersion) { + $regex = '{^'.preg_quote($prefix).'([0-9]+)}$'; + + if (preg_match($regex, PHP_VERSION, $m)) { + return ((int) $m[1]) >= $fixedVersion; + } + + return false; + }; + + // Hard coded list of PHP distributions with the fix backported. + if ( + $compareDistroVersionPrefix('5.3.3-7+squeeze', 19) // Debian 6 (Squeeze) + || $compareDistroVersionPrefix('5.4.4-14+deb7u', 7) // Debian 7 (Wheezy) + || $compareDistroVersionPrefix('5.3.10-1ubuntu3.', 9) // Ubuntu 12.04 (Precise) + ) { + return self::$useOpensslParse = true; + } + + // This is where things get crazy, because distros backport security + // fixes the chances are on NIX systems the fix has been applied but + // it's not possible to verify that from the PHP version. + // + // To verify exec a new PHP process and run the issue testcase with + // known safe input that replicates the bug. + + // Based on testcase in https://github.com/php/php-src/commit/c1224573c773b6845e83505f717fbf820fc18415 + $cert = 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVwRENDQTR5Z0F3SUJBZ0lKQUp6dThyNnU2ZUJjTUEwR0NTcUdTSWIzRFFFQkJRVUFNSUhETVFzd0NRWUQKVlFRR0V3SkVSVEVjTUJvR0ExVUVDQXdUVG05eVpISm9aV2x1TFZkbGMzUm1ZV3hsYmpFUU1BNEdBMVVFQnd3SApTOE9Ed3Jac2JqRVVNQklHQTFVRUNnd0xVMlZyZEdsdmJrVnBibk14SHpBZEJnTlZCQXNNRmsxaGJHbGphVzkxCmN5QkRaWEowSUZObFkzUnBiMjR4SVRBZkJnTlZCQU1NR0cxaGJHbGphVzkxY3k1elpXdDBhVzl1WldsdWN5NWsKWlRFcU1DZ0dDU3FHU0liM0RRRUpBUlliYzNSbFptRnVMbVZ6YzJWeVFITmxhM1JwYjI1bGFXNXpMbVJsTUhVWQpaREU1TnpBd01UQXhNREF3TURBd1dnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBCkFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEKQUFBQUFBQVhEVEUwTVRFeU9ERXhNemt6TlZvd2djTXhDekFKQmdOVkJBWVRBa1JGTVJ3d0dnWURWUVFJREJOTwpiM0prY21obGFXNHRWMlZ6ZEdaaGJHVnVNUkF3RGdZRFZRUUhEQWRMdzRQQ3RteHVNUlF3RWdZRFZRUUtEQXRUClpXdDBhVzl1UldsdWN6RWZNQjBHQTFVRUN3d1dUV0ZzYVdOcGIzVnpJRU5sY25RZ1UyVmpkR2x2YmpFaE1COEcKQTFVRUF3d1liV0ZzYVdOcGIzVnpMbk5sYTNScGIyNWxhVzV6TG1SbE1Tb3dLQVlKS29aSWh2Y05BUWtCRmh0egpkR1ZtWVc0dVpYTnpaWEpBYzJWcmRHbHZibVZwYm5NdVpHVXdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCCkR3QXdnZ0VLQW9JQkFRRERBZjNobDdKWTBYY0ZuaXlFSnBTU0RxbjBPcUJyNlFQNjV1c0pQUnQvOFBhRG9xQnUKd0VZVC9OYSs2ZnNnUGpDMHVLOURaZ1dnMnRIV1dvYW5TYmxBTW96NVBINlorUzRTSFJaN2UyZERJalBqZGhqaAowbUxnMlVNTzV5cDBWNzk3R2dzOWxOdDZKUmZIODFNTjJvYlhXczROdHp0TE11RDZlZ3FwcjhkRGJyMzRhT3M4CnBrZHVpNVVhd1Raa3N5NXBMUEhxNWNNaEZHbTA2djY1Q0xvMFYyUGQ5K0tBb2tQclBjTjVLTEtlYno3bUxwazYKU01lRVhPS1A0aWRFcXh5UTdPN2ZCdUhNZWRzUWh1K3ByWTNzaTNCVXlLZlF0UDVDWm5YMmJwMHdLSHhYMTJEWAoxbmZGSXQ5RGJHdkhUY3lPdU4rblpMUEJtM3ZXeG50eUlJdlZBZ01CQUFHalFqQkFNQWtHQTFVZEV3UUNNQUF3CkVRWUpZSVpJQVliNFFnRUJCQVFEQWdlQU1Bc0dBMVVkRHdRRUF3SUZvREFUQmdOVkhTVUVEREFLQmdnckJnRUYKQlFjREFqQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFHMGZaWVlDVGJkajFYWWMrMVNub2FQUit2SThDOENhRAo4KzBVWWhkbnlVNGdnYTBCQWNEclk5ZTk0ZUVBdTZacXljRjZGakxxWFhkQWJvcHBXb2NyNlQ2R0QxeDMzQ2tsClZBcnpHL0t4UW9oR0QySmVxa2hJTWxEb214SE83a2EzOStPYThpMnZXTFZ5alU4QVp2V01BcnVIYTRFRU55RzcKbFcyQWFnYUZLRkNyOVRuWFRmcmR4R1ZFYnY3S1ZRNmJkaGc1cDVTanBXSDErTXEwM3VSM1pYUEJZZHlWODMxOQpvMGxWajFLRkkyRENML2xpV2lzSlJvb2YrMWNSMzVDdGQwd1lCY3BCNlRac2xNY09QbDc2ZHdLd0pnZUpvMlFnClpzZm1jMnZDMS9xT2xOdU5xLzBUenprVkd2OEVUVDNDZ2FVK1VYZTRYT1Z2a2NjZWJKbjJkZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K'; + $script = <<<'EOT' + +error_reporting(-1); +$info = openssl_x509_parse(base64_decode('%s')); +var_dump(PHP_VERSION, $info['issuer']['emailAddress'], $info['validFrom_time_t']); + +EOT; + $script = '<'."?php\n".sprintf($script, $cert); + + try { + $process = new PhpProcess($script); + $process->mustRun(); + } catch (\Exception $e) { + // In the case of any exceptions just accept it is not possible to + // determine the safety of openssl_x509_parse and bail out. + return self::$useOpensslParse = false; + } + + $output = preg_split('{\r?\n}', trim($process->getOutput())); + $errorOutput = trim($process->getErrorOutput()); + + if ( + count($output) === 3 + && $output[0] === sprintf('string(%d) "%s"', strlen(PHP_VERSION), PHP_VERSION) + && $output[1] === 'string(27) "stefan.esser@sektioneins.de"' + && $output[2] === 'int(-1)' + && preg_match('{openssl_x509_parse\(\): illegal length in timestamp in - on line \d+}', $errorOutput) + ) { + // This PHP has the fix backported probably by a distro security team. + return self::$useOpensslParse = true; + } + + return self::$useOpensslParse = false; + } + + /** + * Convert certificate name into matching function. + * + * @param $certName CN/SAN + * + * @return callable|null + */ + private static function certNameMatcher($certName) + { + $wildcards = substr_count($certName, '*'); + + if (0 === $wildcards) { + // Literal match. + return function ($hostname) use ($certName) { + return $hostname === $certName; + }; + } + + if (1 === $wildcards) { + $components = explode('.', $certName); + + if (3 > count($components)) { + // Must have 3+ components + return; + } + + $firstComponent = $components[0]; + + // Wildcard must be the last character. + if ('*' !== $firstComponent[strlen($firstComponent) - 1]) { + return; + } + + $wildcardRegex = preg_quote($certName); + $wildcardRegex = str_replace('\\*', '[a-z0-9-]+', $wildcardRegex); + $wildcardRegex = "{^{$wildcardRegex}$}"; + + return function ($hostname) use ($wildcardRegex) { + // var_dump($wildcardRegex); + return 1 === preg_match($wildcardRegex, $hostname); + }; + } + } +} diff --git a/tests/Composer/Test/Util/TlsHelperTest.php b/tests/Composer/Test/Util/TlsHelperTest.php new file mode 100644 index 000000000..b17c42ba0 --- /dev/null +++ b/tests/Composer/Test/Util/TlsHelperTest.php @@ -0,0 +1,76 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Util; + +use Composer\Util\TlsHelper; + +class TlsHelperTest extends \PHPUnit_Framework_TestCase +{ + /** @dataProvider dataCheckCertificateHost */ + public function testCheckCertificateHost($expectedResult, $hostname, $certNames) + { + $certificate['subject']['commonName'] = $expectedCn = array_shift($certNames); + $certificate['extensions']['subjectAltName'] = $certNames ? 'DNS:'.implode(',DNS:', $certNames) : ''; + + $result = TlsHelper::checkCertificateHost($certificate, $hostname, $foundCn); + + if (true === $expectedResult) { + $this->assertTrue($result); + $this->assertSame($expectedCn, $foundCn); + } else { + $this->assertFalse($result); + $this->assertNull($foundCn); + } + } + + public function dataCheckCertificateHost() + { + return array( + array(true, 'getcomposer.org', array('getcomposer.org')), + array(true, 'getcomposer.org', array('getcomposer.org', 'packagist.org')), + array(true, 'getcomposer.org', array('packagist.org', 'getcomposer.org')), + array(true, 'foo.getcomposer.org', array('*.getcomposer.org')), + array(false, 'xyz.foo.getcomposer.org', array('*.getcomposer.org')), + array(true, 'foo.getcomposer.org', array('getcomposer.org', '*.getcomposer.org')), + array(true, 'foo.getcomposer.org', array('foo.getcomposer.org', 'foo*.getcomposer.org')), + array(true, 'foo1.getcomposer.org', array('foo.getcomposer.org', 'foo*.getcomposer.org')), + array(true, 'foo2.getcomposer.org', array('foo.getcomposer.org', 'foo*.getcomposer.org')), + array(false, 'foo2.another.getcomposer.org', array('foo.getcomposer.org', 'foo*.getcomposer.org')), + array(false, 'test.example.net', array('**.example.net', '**.example.net')), + array(false, 'test.example.net', array('t*t.example.net', 't*t.example.net')), + array(false, 'xyz.example.org', array('*z.example.org', '*z.example.org')), + array(false, 'foo.bar.example.com', array('foo.*.example.com', 'foo.*.example.com')), + array(false, 'example.com', array('example.*', 'example.*')), + array(true, 'localhost', array('localhost')), + array(false, 'localhost', array('*')), + array(false, 'localhost', array('local*')), + array(false, 'example.net', array('*.net', '*.org', 'ex*.net')), + array(true, 'example.net', array('*.net', '*.org', 'example.net')), + ); + } + + public function testGetCertificateNames() + { + $certificate['subject']['commonName'] = 'example.net'; + $certificate['extensions']['subjectAltName'] = 'DNS: example.com, IP: 127.0.0.1, DNS: getcomposer.org, Junk: blah, DNS: composer.example.org'; + + $names = TlsHelper::getCertificateNames($certificate); + + $this->assertSame('example.net', $names['cn']); + $this->assertSame(array( + 'example.com', + 'getcomposer.org', + 'composer.example.org', + ), $names['san']); + } +} From 74aa73e841f57694907fa81b0f5094a4f5e738a9 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sun, 24 Jan 2016 19:09:35 +0000 Subject: [PATCH 38/98] The origin may not be the remote host --- src/Composer/Util/RemoteFilesystem.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index cee0670c0..afb942850 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -561,11 +561,7 @@ class RemoteFilesystem // Setup remaining TLS options - the matching may need monitoring, esp. www vs none in CN if ($this->disableTls === false && PHP_VERSION_ID < 50600) { - if (!preg_match('{^https?://}', $this->fileUrl)) { - $host = $originUrl; - } else { - $host = parse_url($this->fileUrl, PHP_URL_HOST); - } + $host = parse_url($this->fileUrl, PHP_URL_HOST); if ($host === 'github.com' || $host === 'api.github.com') { $host = '*.github.com'; From b32aad84394dbccef87c0aa114320a7d97873abc Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sun, 24 Jan 2016 19:10:11 +0000 Subject: [PATCH 39/98] Do not set TLS options on local URLs --- src/Composer/Util/RemoteFilesystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index afb942850..09ab6188a 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -560,7 +560,7 @@ class RemoteFilesystem $tlsOptions = array(); // Setup remaining TLS options - the matching may need monitoring, esp. www vs none in CN - if ($this->disableTls === false && PHP_VERSION_ID < 50600) { + if ($this->disableTls === false && PHP_VERSION_ID < 50600 && !stream_is_local($this->fileUrl)) { $host = parse_url($this->fileUrl, PHP_URL_HOST); if ($host === 'github.com' || $host === 'api.github.com') { From bc8b7b0f78f68c0aceed500519e2492492828857 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sun, 24 Jan 2016 19:41:14 +0000 Subject: [PATCH 40/98] Remove left behind debug code --- src/Composer/Util/TlsHelper.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Composer/Util/TlsHelper.php b/src/Composer/Util/TlsHelper.php index ce6738cdd..cfa209e83 100644 --- a/src/Composer/Util/TlsHelper.php +++ b/src/Composer/Util/TlsHelper.php @@ -281,7 +281,6 @@ EOT; $wildcardRegex = "{^{$wildcardRegex}$}"; return function ($hostname) use ($wildcardRegex) { - // var_dump($wildcardRegex); return 1 === preg_match($wildcardRegex, $hostname); }; } From e2e07a32c3fb45c7b63fc33017904b496bac9a2a Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Sun, 24 Jan 2016 20:54:43 +0000 Subject: [PATCH 41/98] Fixes to vuln detection --- src/Composer/Util/TlsHelper.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Composer/Util/TlsHelper.php b/src/Composer/Util/TlsHelper.php index cfa209e83..4aea24df6 100644 --- a/src/Composer/Util/TlsHelper.php +++ b/src/Composer/Util/TlsHelper.php @@ -181,7 +181,7 @@ final class TlsHelper } $compareDistroVersionPrefix = function ($prefix, $fixedVersion) { - $regex = '{^'.preg_quote($prefix).'([0-9]+)}$'; + $regex = '{^'.preg_quote($prefix).'([0-9]+)$}'; if (preg_match($regex, PHP_VERSION, $m)) { return ((int) $m[1]) >= $fixedVersion; @@ -192,7 +192,7 @@ final class TlsHelper // Hard coded list of PHP distributions with the fix backported. if ( - $compareDistroVersionPrefix('5.3.3-7+squeeze', 19) // Debian 6 (Squeeze) + $compareDistroVersionPrefix('5.3.3-7+squeeze', 18) // Debian 6 (Squeeze) || $compareDistroVersionPrefix('5.4.4-14+deb7u', 7) // Debian 7 (Wheezy) || $compareDistroVersionPrefix('5.3.10-1ubuntu3.', 9) // Ubuntu 12.04 (Precise) ) { @@ -207,6 +207,7 @@ final class TlsHelper // known safe input that replicates the bug. // Based on testcase in https://github.com/php/php-src/commit/c1224573c773b6845e83505f717fbf820fc18415 + // changes in https://github.com/php/php-src/commit/76a7fd893b7d6101300cc656058704a73254d593 $cert = 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVwRENDQTR5Z0F3SUJBZ0lKQUp6dThyNnU2ZUJjTUEwR0NTcUdTSWIzRFFFQkJRVUFNSUhETVFzd0NRWUQKVlFRR0V3SkVSVEVjTUJvR0ExVUVDQXdUVG05eVpISm9aV2x1TFZkbGMzUm1ZV3hsYmpFUU1BNEdBMVVFQnd3SApTOE9Ed3Jac2JqRVVNQklHQTFVRUNnd0xVMlZyZEdsdmJrVnBibk14SHpBZEJnTlZCQXNNRmsxaGJHbGphVzkxCmN5QkRaWEowSUZObFkzUnBiMjR4SVRBZkJnTlZCQU1NR0cxaGJHbGphVzkxY3k1elpXdDBhVzl1WldsdWN5NWsKWlRFcU1DZ0dDU3FHU0liM0RRRUpBUlliYzNSbFptRnVMbVZ6YzJWeVFITmxhM1JwYjI1bGFXNXpMbVJsTUhVWQpaREU1TnpBd01UQXhNREF3TURBd1dnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBCkFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEKQUFBQUFBQVhEVEUwTVRFeU9ERXhNemt6TlZvd2djTXhDekFKQmdOVkJBWVRBa1JGTVJ3d0dnWURWUVFJREJOTwpiM0prY21obGFXNHRWMlZ6ZEdaaGJHVnVNUkF3RGdZRFZRUUhEQWRMdzRQQ3RteHVNUlF3RWdZRFZRUUtEQXRUClpXdDBhVzl1UldsdWN6RWZNQjBHQTFVRUN3d1dUV0ZzYVdOcGIzVnpJRU5sY25RZ1UyVmpkR2x2YmpFaE1COEcKQTFVRUF3d1liV0ZzYVdOcGIzVnpMbk5sYTNScGIyNWxhVzV6TG1SbE1Tb3dLQVlKS29aSWh2Y05BUWtCRmh0egpkR1ZtWVc0dVpYTnpaWEpBYzJWcmRHbHZibVZwYm5NdVpHVXdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCCkR3QXdnZ0VLQW9JQkFRRERBZjNobDdKWTBYY0ZuaXlFSnBTU0RxbjBPcUJyNlFQNjV1c0pQUnQvOFBhRG9xQnUKd0VZVC9OYSs2ZnNnUGpDMHVLOURaZ1dnMnRIV1dvYW5TYmxBTW96NVBINlorUzRTSFJaN2UyZERJalBqZGhqaAowbUxnMlVNTzV5cDBWNzk3R2dzOWxOdDZKUmZIODFNTjJvYlhXczROdHp0TE11RDZlZ3FwcjhkRGJyMzRhT3M4CnBrZHVpNVVhd1Raa3N5NXBMUEhxNWNNaEZHbTA2djY1Q0xvMFYyUGQ5K0tBb2tQclBjTjVLTEtlYno3bUxwazYKU01lRVhPS1A0aWRFcXh5UTdPN2ZCdUhNZWRzUWh1K3ByWTNzaTNCVXlLZlF0UDVDWm5YMmJwMHdLSHhYMTJEWAoxbmZGSXQ5RGJHdkhUY3lPdU4rblpMUEJtM3ZXeG50eUlJdlZBZ01CQUFHalFqQkFNQWtHQTFVZEV3UUNNQUF3CkVRWUpZSVpJQVliNFFnRUJCQVFEQWdlQU1Bc0dBMVVkRHdRRUF3SUZvREFUQmdOVkhTVUVEREFLQmdnckJnRUYKQlFjREFqQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFHMGZaWVlDVGJkajFYWWMrMVNub2FQUit2SThDOENhRAo4KzBVWWhkbnlVNGdnYTBCQWNEclk5ZTk0ZUVBdTZacXljRjZGakxxWFhkQWJvcHBXb2NyNlQ2R0QxeDMzQ2tsClZBcnpHL0t4UW9oR0QySmVxa2hJTWxEb214SE83a2EzOStPYThpMnZXTFZ5alU4QVp2V01BcnVIYTRFRU55RzcKbFcyQWFnYUZLRkNyOVRuWFRmcmR4R1ZFYnY3S1ZRNmJkaGc1cDVTanBXSDErTXEwM3VSM1pYUEJZZHlWODMxOQpvMGxWajFLRkkyRENML2xpV2lzSlJvb2YrMWNSMzVDdGQwd1lCY3BCNlRac2xNY09QbDc2ZHdLd0pnZUpvMlFnClpzZm1jMnZDMS9xT2xOdU5xLzBUenprVkd2OEVUVDNDZ2FVK1VYZTRYT1Z2a2NjZWJKbjJkZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K'; $script = <<<'EOT' @@ -234,7 +235,7 @@ EOT; && $output[0] === sprintf('string(%d) "%s"', strlen(PHP_VERSION), PHP_VERSION) && $output[1] === 'string(27) "stefan.esser@sektioneins.de"' && $output[2] === 'int(-1)' - && preg_match('{openssl_x509_parse\(\): illegal length in timestamp in - on line \d+}', $errorOutput) + && preg_match('{openssl_x509_parse\(\): illegal (?:ASN1 data type for|length in) timestamp in - on line \d+}', $errorOutput) ) { // This PHP has the fix backported probably by a distro security team. return self::$useOpensslParse = true; From eb8df89cd5cfaab55e1ec2048f4987e1bf9910fd Mon Sep 17 00:00:00 2001 From: Bob4ever Date: Mon, 25 Jan 2016 14:29:37 +0100 Subject: [PATCH 42/98] Update custom-installers.md --- doc/articles/custom-installers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/articles/custom-installers.md b/doc/articles/custom-installers.md index 8b3536826..a3c937a5e 100644 --- a/doc/articles/custom-installers.md +++ b/doc/articles/custom-installers.md @@ -84,7 +84,7 @@ Example: "class": "phpDocumentor\\Composer\\TemplateInstallerPlugin" }, "require": { - "composer-plugin-api": "1.0.0" + "composer-plugin-api": "^1.0" } } ``` From 901e6f1d0ea72d3bde3af820de221f1f9ea874e6 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 25 Jan 2016 17:55:29 +0000 Subject: [PATCH 43/98] Fix output and handling of RFS::copy() and extract redirect code into its own method, refs #4783 --- src/Composer/Util/RemoteFilesystem.php | 97 ++++++++++++++------------ 1 file changed, 53 insertions(+), 44 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 4644e30f5..699a279fb 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -213,8 +213,10 @@ class RemoteFilesystem unset($additionalOptions['retry-auth-failure']); } + $isRedirect = false; if (isset($additionalOptions['redirects'])) { $this->redirects = $additionalOptions['redirects']; + $isRedirect = true; unset($additionalOptions['redirects']); } @@ -247,7 +249,7 @@ class RemoteFilesystem $ctx = StreamContextFactory::getContext($fileUrl, $options, array('notification' => array($this, 'callbackGet'))); - if ($this->progress) { + if ($this->progress && !$isRedirect) { $this->io->writeError(" Downloading: Connecting...", false); } @@ -295,47 +297,9 @@ class RemoteFilesystem $statusCode = $this->findStatusCode($http_response_header); } - if ($userlandFollow && $statusCode >= 300 && $statusCode <= 399 && $this->redirects < $this->maxRedirects) { - if ($locationHeader = $this->findHeaderValue($http_response_header, 'location')) { - if (parse_url($locationHeader, PHP_URL_SCHEME)) { - // Absolute URL; e.g. https://example.com/composer - $targetUrl = $locationHeader; - } elseif (parse_url($locationHeader, PHP_URL_HOST)) { - // Scheme relative; e.g. //example.com/foo - $targetUrl = $this->scheme.':'.$locationHeader; - } elseif ('/' === $locationHeader[0]) { - // Absolute path; e.g. /foo - $urlHost = parse_url($this->fileUrl, PHP_URL_HOST); - - // Replace path using hostname as an anchor. - $targetUrl = preg_replace('{^(.+(?://|@)'.preg_quote($urlHost).'(?::\d+)?)(?:[/\?].*)?$}', '\1'.$locationHeader, $this->fileUrl); - } else { - // Relative path; e.g. foo - // This actually differs from PHP which seems to add duplicate slashes. - $targetUrl = preg_replace('{^(.+/)[^/?]*(?:\?.*)?$}', '\1'.$locationHeader, $this->fileUrl); - } - } - - if (!empty($targetUrl)) { - $this->redirects++; - - if ($this->io->isDebug()) { - $this->io->writeError(sprintf('Following redirect (%u)', $this->redirects)); - } - - $additionalOptions['redirects'] = $this->redirects; - - // TODO: Not so sure about preserving origin here... - return $this->get($this->originUrl, $targetUrl, $additionalOptions, $this->fileName, $this->progress); - } - - if (!$this->retry) { - $e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded, got redirect without Location ('.$http_response_header[0].')'); - $e->setHeaders($http_response_header); - $e->setResponse($result); - throw $e; - } - $result = false; + // handle 3xx redirects for php<5.6, 304 Not Modified is excluded + if ($userlandFollow && $statusCode >= 300 && $statusCode <= 399 && $statusCode !== 304 && $this->redirects < $this->maxRedirects) { + $result = $this->handleRedirect($http_response_header, $additionalOptions, $result); } // fail 4xx and 5xx responses and capture the response @@ -350,7 +314,7 @@ class RemoteFilesystem $result = false; } - if ($this->progress && !$this->retry) { + if ($this->progress && !$this->retry && !$isRedirect) { $this->io->overwriteError(" Downloading: 100%"); } @@ -387,7 +351,7 @@ class RemoteFilesystem } // handle copy command if download was successful - if (false !== $result && null !== $fileName) { + if (false !== $result && null !== $fileName && !$isRedirect) { if ('' === $result) { throw new TransportException('"'.$this->fileUrl.'" appears broken, and returned an empty 200 response'); } @@ -635,6 +599,51 @@ class RemoteFilesystem return $options; } + private function handleRedirect(array $http_response_header, array $additionalOptions, $result) + { + if ($locationHeader = $this->findHeaderValue($http_response_header, 'location')) { + if (parse_url($locationHeader, PHP_URL_SCHEME)) { + // Absolute URL; e.g. https://example.com/composer + $targetUrl = $locationHeader; + } elseif (parse_url($locationHeader, PHP_URL_HOST)) { + // Scheme relative; e.g. //example.com/foo + $targetUrl = $this->scheme.':'.$locationHeader; + } elseif ('/' === $locationHeader[0]) { + // Absolute path; e.g. /foo + $urlHost = parse_url($this->fileUrl, PHP_URL_HOST); + + // Replace path using hostname as an anchor. + $targetUrl = preg_replace('{^(.+(?://|@)'.preg_quote($urlHost).'(?::\d+)?)(?:[/\?].*)?$}', '\1'.$locationHeader, $this->fileUrl); + } else { + // Relative path; e.g. foo + // This actually differs from PHP which seems to add duplicate slashes. + $targetUrl = preg_replace('{^(.+/)[^/?]*(?:\?.*)?$}', '\1'.$locationHeader, $this->fileUrl); + } + } + + if (!empty($targetUrl)) { + $this->redirects++; + + if ($this->io->isDebug()) { + $this->io->writeError(sprintf('Following redirect (%u) %s', $this->redirects, $targetUrl)); + } + + $additionalOptions['redirects'] = $this->redirects; + + return $this->get($this->originUrl, $targetUrl, $additionalOptions, $this->fileName, $this->progress); + } + + if (!$this->retry) { + $e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded, got redirect without Location ('.$http_response_header[0].')'); + $e->setHeaders($http_response_header); + $e->setResponse($result); + + throw $e; + } + + return false; + } + /** * @param array $options * From e727f9f5feb3136e387ac6eeb22c92bd11f385bb Mon Sep 17 00:00:00 2001 From: Bilal Amarni Date: Fri, 22 Jan 2016 11:08:00 +0100 Subject: [PATCH 44/98] [Config command] allow to pass options when adding a repo --- doc/03-cli.md | 6 ++++++ src/Composer/Command/ConfigCommand.php | 15 ++++++++++++--- ...with-exampletld-repository-and-options.json | 15 +++++++++++++++ .../Test/Config/JsonConfigSourceTest.php | 18 ++++++++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 tests/Composer/Test/Config/Fixtures/config/config-with-exampletld-repository-and-options.json diff --git a/doc/03-cli.md b/doc/03-cli.md index 8c855a286..cb47f7cc4 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -466,6 +466,12 @@ changes to the repositories section by using it the following way: php composer.phar config repositories.foo vcs https://github.com/foo/bar ``` +If your repository requires more configuration options, you can instead pass its JSON representation : + +```sh +php composer.phar config repositories.foo '{"type": "vcs", "url": "http://svn.example.org/my-project/", "trunk-path": "master"}' +``` + ## create-project You can use Composer to create new projects from an existing package. This is diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index 8378bc5b4..6faf5ae2d 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -434,9 +434,18 @@ EOT } if (1 === count($values)) { - $bool = strtolower($values[0]); - if (true === $booleanValidator($bool) && false === $booleanNormalizer($bool)) { - return $this->configSource->addRepository($matches[1], false); + $value = strtolower($values[0]); + if (true === $booleanValidator($value)) { + if (false === $booleanNormalizer($value)) { + return $this->configSource->addRepository($matches[1], false); + } + } else { + $value = json_decode($values[0], true); + if (JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('%s is not valid JSON.', $values[0])); + } + + return $this->configSource->addRepository($matches[1], $value); } } diff --git a/tests/Composer/Test/Config/Fixtures/config/config-with-exampletld-repository-and-options.json b/tests/Composer/Test/Config/Fixtures/config/config-with-exampletld-repository-and-options.json new file mode 100644 index 000000000..c978851c6 --- /dev/null +++ b/tests/Composer/Test/Config/Fixtures/config/config-with-exampletld-repository-and-options.json @@ -0,0 +1,15 @@ +{ + "name": "my-vend/my-app", + "license": "MIT", + "repositories": { + "example_tld": { + "type": "composer", + "url": "https://example.tld", + "options": { + "ssl": { + "local_cert": "/home/composer/.ssl/composer.pem" + } + } + } + } +} diff --git a/tests/Composer/Test/Config/JsonConfigSourceTest.php b/tests/Composer/Test/Config/JsonConfigSourceTest.php index 529532263..819f12f2f 100644 --- a/tests/Composer/Test/Config/JsonConfigSourceTest.php +++ b/tests/Composer/Test/Config/JsonConfigSourceTest.php @@ -52,6 +52,24 @@ class JsonConfigSourceTest extends \PHPUnit_Framework_TestCase $this->assertFileEquals($this->fixturePath('config/config-with-exampletld-repository.json'), $config); } + public function testAddRepositoryWithOptions() + { + $config = $this->workingDir.'/composer.json'; + copy($this->fixturePath('composer-repositories.json'), $config); + $jsonConfigSource = new JsonConfigSource(new JsonFile($config)); + $jsonConfigSource->addRepository('example_tld', array( + 'type' => 'composer', + 'url' => 'https://example.tld', + 'options' => array( + 'ssl' => array( + 'local_cert' => '/home/composer/.ssl/composer.pem' + ) + ) + )); + + $this->assertFileEquals($this->fixturePath('config/config-with-exampletld-repository-and-options.json'), $config); + } + public function testRemoveRepository() { $config = $this->workingDir.'/composer.json'; From 78ffe0fd087e3e2941c7d85b71f294985a8007ad Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 25 Jan 2016 18:34:52 +0000 Subject: [PATCH 45/98] Avoid checking CA files several times --- src/Composer/Util/RemoteFilesystem.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 699a279fb..81f18fa19 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -842,6 +842,12 @@ class RemoteFilesystem */ private function validateCaFile($filename) { + static $files = array(); + + if (isset($files[$filename])) { + return $files[$filename]; + } + if ($this->io->isDebug()) { $this->io->writeError('Checking CA file '.realpath($filename)); } @@ -854,10 +860,10 @@ class RemoteFilesystem || (PHP_VERSION_ID >= 50400 && PHP_VERSION_ID < 50422) || (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50506) ) { - return !empty($contents); + return $files[$filename] = !empty($contents); } - return (bool) openssl_x509_parse($contents); + return $files[$filename] = (bool) openssl_x509_parse($contents); } /** From bdb97e752713b4d492b7e0884e67fd51e81583df Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 25 Jan 2016 19:17:56 +0000 Subject: [PATCH 46/98] Reuse new TlsHelper for CA validation, refs #4798 --- src/Composer/Util/RemoteFilesystem.php | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 9fa54590e..b934bd145 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -383,6 +383,7 @@ class RemoteFilesystem } } + // Handle SSL cert match issues if (false === $result && false !== strpos($errorMessage, 'Peer certificate') && PHP_VERSION_ID < 50600) { // Certificate name error, PHP doesn't support subjectAltName on PHP < 5.6 // The procedure to handle sAN for older PHP's is: @@ -421,9 +422,11 @@ class RemoteFilesystem $result = $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress); - $authHelper = new AuthHelper($this->io, $this->config); - $authHelper->storeAuth($this->originUrl, $this->storeAuth); - $this->storeAuth = false; + if (false !== $this->storeAuth) { + $authHelper = new AuthHelper($this->io, $this->config); + $authHelper->storeAuth($this->originUrl, $this->storeAuth); + $this->storeAuth = false; + } return $result; } @@ -734,7 +737,7 @@ class RemoteFilesystem 'DHE-DSS-AES256-SHA', 'DHE-RSA-AES256-SHA', 'AES128-GCM-SHA256', - 'AES256-GCM-SHA384', + 'AES256-GCM-SHA384', 'ECDHE-RSA-RC4-SHA', 'ECDHE-ECDSA-RC4-SHA', 'AES128', @@ -916,11 +919,12 @@ class RemoteFilesystem // assume the CA is valid if php is vulnerable to // https://www.sektioneins.de/advisories/advisory-012013-php-openssl_x509_parse-memory-corruption-vulnerability.html - if ( - PHP_VERSION_ID <= 50327 - || (PHP_VERSION_ID >= 50400 && PHP_VERSION_ID < 50422) - || (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50506) - ) { + if (!TlsHelper::isOpensslParseSafe()) { + $this->io->writeError(sprintf( + 'Your version of PHP, %s, is affected by CVE-2013-6420 and cannot safely perform certificate validation, we strongly suggest you upgrade.', + PHP_VERSION + )); + return $files[$filename] = !empty($contents); } From a9be7c83f13211f8edc2693e13c449e6efff5adb Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 16 Jan 2016 17:13:41 +0000 Subject: [PATCH 47/98] Add verification of signatures when running self-update --- src/Composer/Command/SelfUpdateCommand.php | 96 +++++++++++++++++++++- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 581c45ab6..f227b22dc 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -14,7 +14,9 @@ namespace Composer\Command; use Composer\Composer; use Composer\Factory; +use Composer\Config; use Composer\Util\Filesystem; +use Composer\IO\IOInterface; use Composer\Util\RemoteFilesystem; use Composer\Downloader\FilesystemException; use Symfony\Component\Console\Input\InputInterface; @@ -44,6 +46,7 @@ class SelfUpdateCommand extends Command new InputOption('clean-backups', null, InputOption::VALUE_NONE, 'Delete old backups during an update. This makes the current version of composer the only backup available after the update'), new InputArgument('version', InputArgument::OPTIONAL, 'The version to update to'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), + new InputOption('update-keys', null, InputOption::VALUE_NONE, 'Prompt user for a key update'), )) ->setHelp(<<self-update command checks getcomposer.org for newer @@ -71,8 +74,13 @@ EOT $cacheDir = $config->get('cache-dir'); $rollbackDir = $config->get('data-dir'); + $home = $config->get('home'); $localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0]; + if ($input->getOption('update-keys')) { + return $this->fetchKeys($io, $config); + } + // check if current dir is writable and if not try the cache dir from settings $tmpDir = is_writable(dirname($localFilename)) ? dirname($localFilename) : $cacheDir; @@ -112,15 +120,55 @@ EOT self::OLD_INSTALL_EXT ); - $io->writeError(sprintf("Updating to version %s.", $updateVersion)); - $remoteFilename = $baseUrl . (preg_match('{^[0-9a-f]{40}$}', $updateVersion) ? '/composer.phar' : "/download/{$updateVersion}/composer.phar"); + $updatingToTag = !preg_match('{^[0-9a-f]{40}$}', $updateVersion); + + $io->write(sprintf("Updating to version %s.", $updateVersion)); + $remoteFilename = $baseUrl . ($updatingToTag ? "/download/{$updateVersion}/composer.phar" : '/composer.phar'); + $signature = $remoteFilesystem->getContents(self::HOMEPAGE, $remoteFilename.'.sig', false); $remoteFilesystem->copy(self::HOMEPAGE, $remoteFilename, $tempFilename, !$input->getOption('no-progress')); - if (!file_exists($tempFilename)) { + if (!file_exists($tempFilename) || !$signature) { $io->writeError('The download of the new composer version failed for an unexpected reason'); return 1; } + // verify phar signature + if (!extension_loaded('openssl') && $config->get('disable-tls')) { + $io->writeError('Skipping phar signature verification as you have disabled OpenSSL via config.disable-tls'); + } else { + if (!extension_loaded('openssl')) { + throw new \RuntimeException('The openssl extension is required for phar signatures to be verified but it is not available. ' + . 'If you can not enable the openssl extension, you can disable this error, at your own risk, by setting the \'disable-tls\' option to true.'); + } + + $sigFile = 'file://'.$home.'/' . ($updatingToTag ? 'keys.tags.pub' : 'keys.dev.pub'); + if (!file_exists($sigFile)) { + $io->write('You are missing the public keys used to verify Composer phar file signatures'); + if (!$io->isInteractive() || getenv('CI') || getenv('CONTINUOUS_INTEGRATION')) { + $io->write('As this process is not interactive or you run on CI, it is allowed to run for now, but you should run "composer self-update --update-keys" to get them set up.'); + } else { + $this->fetchKeys($io, $config); + } + } + + // if still no file is present it means we are on CI/travis or + // not interactive so we skip the check for now + if (file_exists($sigFile)) { + $pubkeyid = openssl_pkey_get_public($sigFile); + $algo = defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'SHA384'; + if (!in_array('SHA384', openssl_get_md_methods())) { + throw new \RuntimeException('SHA384 is not supported by your openssl extension, could not verify the phar file integrity'); + } + $signature = json_decode($signature, true); + $signature = base64_decode($signature['sha384']); + $verified = 1 === openssl_verify(file_get_contents($tempFilename), $signature, $pubkeyid, $algo); + openssl_free_key($pubkeyid); + if (!$verified) { + throw new \RuntimeException('The phar signature did not match the file you downloaded, this means your public keys are outdated or that the phar file is corrupt/has been modified'); + } + } + } + // remove saved installations of composer if ($input->getOption('clean-backups')) { $finder = $this->getOldInstallationFinder($rollbackDir); @@ -147,6 +195,48 @@ EOT } } + protected function fetchKeys(IOInterface $io, Config $config) + { + if (!$io->isInteractive()) { + throw new \RuntimeException('Public keys are missing and can not be fetched in non-interactive mode, run this interactively or re-install composer using the installer to get the public keys set up'); + } + + $io->write('Open https://composer.github.io/pubkeys.html to find the latest keys'); + + $validator = function ($value) { + if (!preg_match('{^-----BEGIN PUBLIC KEY-----$}', trim($value))) { + throw new \UnexpectedValueException('Invalid input'); + } + return trim($value)."\n"; + }; + + $devKey = ''; + while (!preg_match('{(-----BEGIN PUBLIC KEY-----.+?-----END PUBLIC KEY-----)}s', $devKey, $match)) { + $devKey = $io->askAndValidate('Enter Dev / Snapshot Public Key (including lines with -----): ', $validator); + while ($line = $io->ask('')) { + $devKey .= trim($line)."\n"; + if (trim($line) === '-----END PUBLIC KEY-----') { + break; + } + } + } + file_put_contents($config->get('home').'/keys.dev.pub', $match[0]); + + $tagsKey = ''; + while (!preg_match('{(-----BEGIN PUBLIC KEY-----.+?-----END PUBLIC KEY-----)}s', $tagsKey, $match)) { + $tagsKey = $io->askAndValidate('Enter Tags Public Key (including lines with -----): ', $validator); + while ($line = $io->ask('')) { + $tagsKey .= trim($line)."\n"; + if (trim($line) === '-----END PUBLIC KEY-----') { + break; + } + } + } + file_put_contents($config->get('home').'/keys.tags.pub', $match[0]); + + $io->write('Public keys stored in '.$config->get('home')); + } + protected function rollback(OutputInterface $output, $rollbackDir, $localFilename) { $rollbackVersion = $this->getLastBackupVersion($rollbackDir); From 3ef22258e5f2674a4259b60e2eb0377ccd145d26 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sun, 17 Jan 2016 14:49:58 +0000 Subject: [PATCH 48/98] Add key fingerprints for easier comparison and debugging via diagnose --- src/Composer/Command/DiagnoseCommand.php | 33 ++++++++++++++++++++++ src/Composer/Command/SelfUpdateCommand.php | 7 +++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index faf537a10..b3ecb08bb 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -22,6 +22,7 @@ use Composer\Util\ConfigValidator; use Composer\Util\ProcessExecutor; use Composer\Util\RemoteFilesystem; use Composer\Util\StreamContextFactory; +use Composer\Util\Keys; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -133,6 +134,9 @@ EOT $io->write('Checking disk free space: ', false); $this->outputResult($this->checkDiskSpace($config)); + $io->write('Checking pubkeys: ', false); + $this->outputResult($this->checkPubKeys($config)); + $io->write('Checking composer version: ', false); $this->outputResult($this->checkVersion()); @@ -327,6 +331,35 @@ EOT return true; } + private function checkPubKeys($config) + { + $home = $config->get('home'); + $errors = []; + $io = $this->getIO(); + + if (file_exists($home.'/keys.tags.pub') && file_exists($home.'/keys.dev.pub')) { + $io->write(''); + } + + if (file_exists($home.'/keys.tags.pub')) { + $io->write('Tags Public Key Fingerprint: ' . Keys::fingerprint($home.'/keys.tags.pub')); + } else { + $errors[] = 'Missing pubkey for tags verification'; + } + + if (file_exists($home.'/keys.dev.pub')) { + $io->write('Dev Public Key Fingerprint: ' . Keys::fingerprint($home.'/keys.dev.pub')); + } else { + $errors[] = 'Missing pubkey for dev verification'; + } + + if ($errors) { + $errors[] = 'Run composer self-update --update-keys to set them up'; + } + + return $errors ?: true; + } + private function checkVersion() { $protocol = extension_loaded('openssl') ? 'https' : 'http'; diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index f227b22dc..3f456862c 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -16,6 +16,7 @@ use Composer\Composer; use Composer\Factory; use Composer\Config; use Composer\Util\Filesystem; +use Composer\Util\Keys; use Composer\IO\IOInterface; use Composer\Util\RemoteFilesystem; use Composer\Downloader\FilesystemException; @@ -220,7 +221,8 @@ EOT } } } - file_put_contents($config->get('home').'/keys.dev.pub', $match[0]); + file_put_contents($keyPath = $config->get('home').'/keys.dev.pub', $match[0]); + $io->write('Stored key with fingerprint: ' . Keys::fingerprint($keyPath)); $tagsKey = ''; while (!preg_match('{(-----BEGIN PUBLIC KEY-----.+?-----END PUBLIC KEY-----)}s', $tagsKey, $match)) { @@ -232,7 +234,8 @@ EOT } } } - file_put_contents($config->get('home').'/keys.tags.pub', $match[0]); + file_put_contents($keyPath = $config->get('home').'/keys.tags.pub', $match[0]); + $io->write('Stored key with fingerprint: ' . Keys::fingerprint($keyPath)); $io->write('Public keys stored in '.$config->get('home')); } From f4bcf7590b131332148de6df77ab452e47bb456e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 18 Jan 2016 11:14:38 +0000 Subject: [PATCH 49/98] Fix array syntax --- src/Composer/Command/DiagnoseCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index b3ecb08bb..2306b138a 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -334,7 +334,7 @@ EOT private function checkPubKeys($config) { $home = $config->get('home'); - $errors = []; + $errors = array(); $io = $this->getIO(); if (file_exists($home.'/keys.tags.pub') && file_exists($home.'/keys.dev.pub')) { From 59975e3aaa1083c574ca934ee618ecd5bdbdfc96 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 18 Jan 2016 13:39:50 +0000 Subject: [PATCH 50/98] Add missing keys class --- src/Composer/Util/Keys.php | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/Composer/Util/Keys.php diff --git a/src/Composer/Util/Keys.php b/src/Composer/Util/Keys.php new file mode 100644 index 000000000..19628f5d3 --- /dev/null +++ b/src/Composer/Util/Keys.php @@ -0,0 +1,38 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +use Composer\Config; + +/** + * @author Jordi Boggiano + */ +class Keys +{ + public static function fingerprint($path) + { + $hash = strtoupper(hash('sha256', preg_replace('{\s}', '', file_get_contents($path)))); + + return implode(' ', [ + substr($hash, 0, 8), + substr($hash, 8, 8), + substr($hash, 16, 8), + substr($hash, 24, 8), + '', // Extra space + substr($hash, 32, 8), + substr($hash, 40, 8), + substr($hash, 48, 8), + substr($hash, 56, 8), + ]); + } +} From 5b41eaad3aecab692cbafc2b9cf720ed6bde74a0 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 25 Jan 2016 19:35:11 +0000 Subject: [PATCH 51/98] Bundle pubkeys and fail hard if validation can not happen --- src/Composer/Command/SelfUpdateCommand.php | 68 +++++++++++++++------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 3f456862c..7d868da06 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -144,29 +144,53 @@ EOT $sigFile = 'file://'.$home.'/' . ($updatingToTag ? 'keys.tags.pub' : 'keys.dev.pub'); if (!file_exists($sigFile)) { - $io->write('You are missing the public keys used to verify Composer phar file signatures'); - if (!$io->isInteractive() || getenv('CI') || getenv('CONTINUOUS_INTEGRATION')) { - $io->write('As this process is not interactive or you run on CI, it is allowed to run for now, but you should run "composer self-update --update-keys" to get them set up.'); - } else { - $this->fetchKeys($io, $config); - } + file_put_contents($home.'/keys.dev.pub', <<isInteractive()) { - throw new \RuntimeException('Public keys are missing and can not be fetched in non-interactive mode, run this interactively or re-install composer using the installer to get the public keys set up'); + throw new \RuntimeException('Public keys can not be fetched in non-interactive mode, please run Composer interactively'); } $io->write('Open https://composer.github.io/pubkeys.html to find the latest keys'); From e0ff9598c3a52468cf0dbbae386b883217552e31 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 25 Jan 2016 22:24:34 +0000 Subject: [PATCH 52/98] Tweak wording a bit, refs #3177 --- src/Composer/DependencyResolver/SolverProblemsException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index 1dfc116f0..fcbb6a77b 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -72,7 +72,7 @@ class SolverProblemsException extends \RuntimeException return ''; } - $text = "\n Because of missing extensions, please verify whether they are enabled in those .ini files:\n - "; + $text = "\n To enable extensions, verify that they are enabled in those .ini files:\n - "; $text .= implode("\n - ", $paths); $text .= "\n You can also run `php --ini` inside terminal to see which files are used by PHP in CLI mode."; From 18cd4f966bf8f95929cbcad20f9d08871e9bba35 Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Mon, 25 Jan 2016 23:37:54 +0100 Subject: [PATCH 53/98] Added silencer utility to more gracefully handle error suppression without hiding errors or worse. Fixes #4203, #4683 --- src/Composer/Autoload/ClassMapGenerator.php | 3 +- src/Composer/Cache.php | 3 +- src/Composer/Command/ConfigCommand.php | 11 ++- src/Composer/Command/CreateProjectCommand.php | 7 +- src/Composer/Config/JsonConfigSource.php | 3 +- src/Composer/Console/Application.php | 11 ++- src/Composer/Factory.php | 5 +- src/Composer/Installer/LibraryInstaller.php | 9 ++- src/Composer/Util/RemoteFilesystem.php | 2 +- src/Composer/Util/Silencer.php | 73 +++++++++++++++++++ tests/Composer/Test/Util/SilencerTest.php | 57 +++++++++++++++ 11 files changed, 164 insertions(+), 20 deletions(-) create mode 100644 src/Composer/Util/Silencer.php create mode 100644 tests/Composer/Test/Util/SilencerTest.php diff --git a/src/Composer/Autoload/ClassMapGenerator.php b/src/Composer/Autoload/ClassMapGenerator.php index b487ed6ed..3f1243ade 100644 --- a/src/Composer/Autoload/ClassMapGenerator.php +++ b/src/Composer/Autoload/ClassMapGenerator.php @@ -18,6 +18,7 @@ namespace Composer\Autoload; +use Composer\Util\Silencer; use Symfony\Component\Finder\Finder; use Composer\IO\IOInterface; @@ -122,7 +123,7 @@ class ClassMapGenerator } try { - $contents = @php_strip_whitespace($path); + $contents = Silencer::call('php_strip_whitespace', $path); if (!$contents) { if (!file_exists($path)) { throw new \Exception('File does not exist'); diff --git a/src/Composer/Cache.php b/src/Composer/Cache.php index 3ba11da1c..8c5bce4ee 100644 --- a/src/Composer/Cache.php +++ b/src/Composer/Cache.php @@ -14,6 +14,7 @@ namespace Composer; use Composer\IO\IOInterface; use Composer\Util\Filesystem; +use Composer\Util\Silencer; use Symfony\Component\Finder\Finder; /** @@ -44,7 +45,7 @@ class Cache $this->filesystem = $filesystem ?: new Filesystem(); if ( - (!is_dir($this->root) && !@mkdir($this->root, 0777, true)) + (!is_dir($this->root) && !Silencer::call('mkdir', $this->root, 0777, true)) || !is_writable($this->root) ) { $this->io->writeError('Cannot create cache directory ' . $this->root . ', or directory is not writable. Proceeding without cache'); diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index 0bf8f97d5..eb1778a94 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -12,6 +12,7 @@ namespace Composer\Command; +use Composer\Util\Silencer; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -142,7 +143,7 @@ EOT ? ($this->config->get('home') . '/config.json') : ($input->getOption('file') ?: trim(getenv('COMPOSER')) ?: 'composer.json'); - // create global composer.json if this was invoked using `composer global config` + // Create global composer.json if this was invoked using `composer global config` if ($configFile === 'composer.json' && !file_exists($configFile) && realpath(getcwd()) === realpath($this->config->get('home'))) { file_put_contents($configFile, "{\n}\n"); } @@ -157,17 +158,19 @@ EOT $this->authConfigFile = new JsonFile($authConfigFile, null, $io); $this->authConfigSource = new JsonConfigSource($this->authConfigFile, true); - // initialize the global file if it's not there + // Initialize the global file if it's not there, ignoring any warnings or notices + Silencer::suppress(); if ($input->getOption('global') && !$this->configFile->exists()) { touch($this->configFile->getPath()); $this->configFile->write(array('config' => new \ArrayObject)); - @chmod($this->configFile->getPath(), 0600); + chmod($this->configFile->getPath(), 0600); } if ($input->getOption('global') && !$this->authConfigFile->exists()) { touch($this->authConfigFile->getPath()); $this->authConfigFile->write(array('http-basic' => new \ArrayObject, 'github-oauth' => new \ArrayObject, 'gitlab-oauth' => new \ArrayObject)); - @chmod($this->authConfigFile->getPath(), 0600); + chmod($this->authConfigFile->getPath(), 0600); } + Silencer::restore(); if (!$this->configFile->exists()) { throw new \RuntimeException(sprintf('File "%s" cannot be found in the current directory', $configFile)); diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 144d850a6..df50c4750 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -27,6 +27,7 @@ use Composer\Repository\CompositeRepository; use Composer\Repository\FilesystemRepository; use Composer\Repository\InstalledFilesystemRepository; use Composer\Script\ScriptEvents; +use Composer\Util\Silencer; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -224,11 +225,13 @@ EOT chdir($oldCwd); $vendorComposerDir = $composer->getConfig()->get('vendor-dir').'/composer'; if (is_dir($vendorComposerDir) && $fs->isDirEmpty($vendorComposerDir)) { - @rmdir($vendorComposerDir); + Silencer::suppress(); + rmdir($vendorComposerDir); $vendorDir = $composer->getConfig()->get('vendor-dir'); if (is_dir($vendorDir) && $fs->isDirEmpty($vendorDir)) { - @rmdir($vendorDir); + rmdir($vendorDir); } + Silencer::restore(); } return 0; diff --git a/src/Composer/Config/JsonConfigSource.php b/src/Composer/Config/JsonConfigSource.php index 5df29f032..2b6d13096 100644 --- a/src/Composer/Config/JsonConfigSource.php +++ b/src/Composer/Config/JsonConfigSource.php @@ -14,6 +14,7 @@ namespace Composer\Config; use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; +use Composer\Util\Silencer; /** * JSON Configuration Source @@ -173,7 +174,7 @@ class JsonConfigSource implements ConfigSourceInterface } if ($newFile) { - @chmod($this->file->getPath(), 0600); + Silencer::call('chmod', $this->file->getPath(), 0600); } } diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 756cf18c6..10ad2762b 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -12,6 +12,7 @@ namespace Composer\Console; +use Composer\Util\Silencer; use Symfony\Component\Console\Application as BaseApplication; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -64,7 +65,7 @@ class Application extends BaseApplication } if (function_exists('date_default_timezone_set') && function_exists('date_default_timezone_get')) { - date_default_timezone_set(@date_default_timezone_get()); + date_default_timezone_set(Silencer::call('date_default_timezone_get')); } if (!$shutdownRegistered) { @@ -203,21 +204,23 @@ class Application extends BaseApplication { $io = $this->getIO(); + Silencer::suppress(); try { $composer = $this->getComposer(false, true); if ($composer) { $config = $composer->getConfig(); $minSpaceFree = 1024 * 1024; - if ((($df = @disk_free_space($dir = $config->get('home'))) !== false && $df < $minSpaceFree) - || (($df = @disk_free_space($dir = $config->get('vendor-dir'))) !== false && $df < $minSpaceFree) - || (($df = @disk_free_space($dir = sys_get_temp_dir())) !== false && $df < $minSpaceFree) + if ((($df = disk_free_space($dir = $config->get('home'))) !== false && $df < $minSpaceFree) + || (($df = disk_free_space($dir = $config->get('vendor-dir'))) !== false && $df < $minSpaceFree) + || (($df = disk_free_space($dir = sys_get_temp_dir())) !== false && $df < $minSpaceFree) ) { $io->writeError('The disk hosting '.$dir.' is full, this may be the cause of the following exception'); } } } catch (\Exception $e) { } + Silencer::restore(); if (defined('PHP_WINDOWS_VERSION_BUILD') && false !== strpos($exception->getMessage(), 'The system cannot find the path specified')) { $io->writeError('The following exception may be caused by a stale entry in your cmd.exe AutoRun'); diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index e6718278f..eb6072709 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -22,6 +22,7 @@ use Composer\Repository\WritableRepositoryInterface; use Composer\Util\Filesystem; use Composer\Util\ProcessExecutor; use Composer\Util\RemoteFilesystem; +use Composer\Util\Silencer; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Composer\EventDispatcher\EventDispatcher; use Composer\Autoload\AutoloadGenerator; @@ -163,9 +164,9 @@ class Factory foreach ($dirs as $dir) { if (!file_exists($dir . '/.htaccess')) { if (!is_dir($dir)) { - @mkdir($dir, 0777, true); + Silencer::call('mkdir', $dir, 0777, true); } - @file_put_contents($dir . '/.htaccess', 'Deny from all'); + Silencer::call('file_put_contents', $dir . '/.htaccess', 'Deny from all'); } } diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index 20a281de7..b14659f7b 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -18,6 +18,7 @@ use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; use Composer\Util\Filesystem; use Composer\Util\ProcessExecutor; +use Composer\Util\Silencer; /** * Package installation manager. @@ -130,7 +131,7 @@ class LibraryInstaller implements InstallerInterface if (strpos($package->getName(), '/')) { $packageVendorDir = dirname($downloadPath); if (is_dir($packageVendorDir) && $this->filesystem->isDirEmpty($packageVendorDir)) { - @rmdir($packageVendorDir); + Silencer::call('rmdir', $packageVendorDir); } } } @@ -233,7 +234,7 @@ class LibraryInstaller implements InstallerInterface // likely leftover from a previous install, make sure // that the target is still executable in case this // is a fresh install of the vendor. - @chmod($link, 0777 & ~umask()); + Silencer::call('chmod', $link, 0777 & ~umask()); } $this->io->writeError(' Skipped installation of bin '.$bin.' for package '.$package->getName().': name conflicts with an existing file'); continue; @@ -248,7 +249,7 @@ class LibraryInstaller implements InstallerInterface } elseif ($this->binCompat === "full") { $this->installFullBinaries($binPath, $link, $bin, $package); } - @chmod($link, 0777 & ~umask()); + Silencer::call('chmod', $link, 0777 & ~umask()); } } @@ -298,7 +299,7 @@ class LibraryInstaller implements InstallerInterface // attempt removing the bin dir in case it is left empty if ((is_dir($this->binDir)) && ($this->filesystem->isDirEmpty($this->binDir))) { - @rmdir($this->binDir); + Silencer::call('rmdir', $this->binDir); } } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 9a8a25d81..e62ec9d18 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -743,7 +743,7 @@ class RemoteFilesystem ); foreach ($caBundlePaths as $caBundle) { - if (@is_readable($caBundle) && $this->validateCaFile($caBundle)) { + if (Silencer::call('is_readable', $caBundle) && $this->validateCaFile($caBundle)) { return $caPath = $caBundle; } } diff --git a/src/Composer/Util/Silencer.php b/src/Composer/Util/Silencer.php new file mode 100644 index 000000000..bc09d5efd --- /dev/null +++ b/src/Composer/Util/Silencer.php @@ -0,0 +1,73 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +/** + * Temporarily suppress PHP error reporting, usually warnings and below. + * + * @author Niels Keurentjes + */ +class Silencer +{ + /** + * @var int[] Unpop stack + */ + private static $stack = array(); + + /** + * Suppresses given mask or errors. + * + * @param int|null $mask Error levels to suppress, default value NULL indicates all warnings and below. + * @return int The old error reporting level. + */ + public static function suppress($mask = null) + { + if (!isset($mask)) { + $mask = E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_DEPRECATED | E_USER_DEPRECATED | E_STRICT; + } + array_push(self::$stack, $old = error_reporting()); + error_reporting($old & ~$mask); + return $old; + } + + /** + * Restores a single state. + */ + public static function restore() + { + if (!empty(self::$stack)) + error_reporting(array_pop(self::$stack)); + } + + /** + * Calls a specified function while silencing warnings and below. + * + * Future improvement: when PHP requirements are raised add Callable type hint (5.4) and variadic parameters (5.6) + * + * @param callable $callable Function to execute. + * @return mixed Return value of the callback. + * @throws \Exception Any exceptions from the callback are rethrown. + */ + public static function call($callable /*, ...$parameters */) + { + try { + self::suppress(); + $result = call_user_func_array($callable, array_slice(func_get_args(), 1)); + self::restore(); + return $result; + } catch(\Exception $e) { + // Use a finally block for this when requirements are raised to PHP 5.5 + self::restore(); + throw $e; + } + } +} \ No newline at end of file diff --git a/tests/Composer/Test/Util/SilencerTest.php b/tests/Composer/Test/Util/SilencerTest.php new file mode 100644 index 000000000..2926cc2a4 --- /dev/null +++ b/tests/Composer/Test/Util/SilencerTest.php @@ -0,0 +1,57 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Util; + +use Composer\Util\Silencer; + +/** + * SilencerTest + * + * @author Niels Keurentjes + */ +class SilencerTest extends \PHPUnit_Framework_TestCase +{ + /** + * Test succeeds when no warnings are emitted externally, and original level is restored. + */ + public function testSilencer() + { + $before = error_reporting(); + + // Check warnings are suppressed correctly + Silencer::suppress(); + trigger_error('Test', E_USER_WARNING); + Silencer::restore(); + + // Check all parameters and return values are passed correctly in a silenced call. + $result = Silencer::call(function($a, $b, $c) { + trigger_error('Test', E_USER_WARNING); + return $a * $b * $c; + }, 2, 3, 4); + $this->assertEquals(24, $result); + + // Check the error reporting setting was restored correctly + $this->assertEquals($before, error_reporting()); + } + + /** + * Test whether exception from silent callbacks are correctly forwarded. + */ + public function testSilencedException() + { + $verification = microtime(); + $this->setExpectedException('\RuntimeException', $verification); + Silencer::call(function() use ($verification) { + throw new \RuntimeException($verification); + }); + } +} From 2c3e7cf5f29a4bd896e02987ade0878f51d99cbe Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Mon, 25 Jan 2016 23:51:11 +0100 Subject: [PATCH 54/98] Unit tests fail in a nasty way if ErrorHandler test is run before the Silencer and it's not silencing itself. --- tests/Composer/Test/Util/SilencerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Composer/Test/Util/SilencerTest.php b/tests/Composer/Test/Util/SilencerTest.php index 2926cc2a4..3bf913480 100644 --- a/tests/Composer/Test/Util/SilencerTest.php +++ b/tests/Composer/Test/Util/SilencerTest.php @@ -29,12 +29,12 @@ class SilencerTest extends \PHPUnit_Framework_TestCase // Check warnings are suppressed correctly Silencer::suppress(); - trigger_error('Test', E_USER_WARNING); + @trigger_error('Test', E_USER_WARNING); Silencer::restore(); // Check all parameters and return values are passed correctly in a silenced call. $result = Silencer::call(function($a, $b, $c) { - trigger_error('Test', E_USER_WARNING); + @trigger_error('Test', E_USER_WARNING); return $a * $b * $c; }, 2, 3, 4); $this->assertEquals(24, $result); From 84fed02df11541a0e7e91192f9daa5a2f18fed22 Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Tue, 26 Jan 2016 00:33:47 +0100 Subject: [PATCH 55/98] Globbing while resolving path repositories now normalizes to slashes for predictable cross-platform behaviour. Fixes #4726 --- src/Composer/Repository/PathRepository.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Composer/Repository/PathRepository.php b/src/Composer/Repository/PathRepository.php index b826ca999..4529af0f0 100644 --- a/src/Composer/Repository/PathRepository.php +++ b/src/Composer/Repository/PathRepository.php @@ -113,7 +113,7 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn parent::initialize(); foreach ($this->getUrlMatches() as $url) { - $path = realpath($url) . '/'; + $path = realpath($url) . DIRECTORY_SEPARATOR; $composerFilePath = $path.'composer.json'; if (!file_exists($composerFilePath)) { @@ -131,7 +131,8 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn if (!isset($package['version'])) { $package['version'] = $this->versionGuesser->guessVersion($package, $path) ?: 'dev-master'; } - if (is_dir($path.'/.git') && 0 === $this->process->execute('git log -n1 --pretty=%H', $output, $path)) { + $output = ''; + if (is_dir($path . DIRECTORY_SEPARATOR . '.git') && 0 === $this->process->execute('git log -n1 --pretty=%H', $output, $path)) { $package['dist']['reference'] = trim($output); } else { $package['dist']['reference'] = Locker::getContentHash($json); @@ -153,6 +154,9 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn */ private function getUrlMatches() { - return glob($this->url, GLOB_MARK | GLOB_ONLYDIR); + // Ensure environment-specific path separators are normalized to URL separators + return array_map(function($val) { + return str_replace(DIRECTORY_SEPARATOR, '/', $val); + }, glob($this->url, GLOB_MARK | GLOB_ONLYDIR)); } } From 03e0d65f378976767dde7d59e2ace39bc914dbfd Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 25 Jan 2016 23:40:16 +0000 Subject: [PATCH 56/98] Allow stream wrapper usage in config dirs, fixes #4788 --- src/Composer/Config.php | 2 +- tests/Composer/Test/ConfigTest.php | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Composer/Config.php b/src/Composer/Config.php index a1b0246b9..e8fdf2561 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -345,7 +345,7 @@ class Config */ private function realpath($path) { - if (substr($path, 0, 1) === '/' || substr($path, 1, 1) === ':') { + if (preg_match('{^(?:/|[a-z]:|[a-z0-9.]+://)}i', $path)) { return $path; } diff --git a/tests/Composer/Test/ConfigTest.php b/tests/Composer/Test/ConfigTest.php index 88d8a15d3..ca3e54ce7 100644 --- a/tests/Composer/Test/ConfigTest.php +++ b/tests/Composer/Test/ConfigTest.php @@ -148,6 +148,16 @@ class ConfigTest extends \PHPUnit_Framework_TestCase $this->assertEquals('/baz', $config->get('cache-dir')); } + public function testStreamWrapperDirs() + { + $config = new Config(false, '/foo/bar'); + $config->merge(array('config' => array( + 'cache-dir' => 's3://baz/', + ))); + + $this->assertEquals('s3://baz', $config->get('cache-dir')); + } + public function testFetchingRelativePaths() { $config = new Config(false, '/foo/bar'); From aef4820abec037e9d182f64c04327b881eb11e05 Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Tue, 26 Jan 2016 09:08:57 +0100 Subject: [PATCH 57/98] Normalization of URLs caused discrepancy on Windows with unit tests. --- tests/Composer/Test/Repository/PathRepositoryTest.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/Composer/Test/Repository/PathRepositoryTest.php b/tests/Composer/Test/Repository/PathRepositoryTest.php index 47b7ac24f..e76be2bfa 100644 --- a/tests/Composer/Test/Repository/PathRepositoryTest.php +++ b/tests/Composer/Test/Repository/PathRepositoryTest.php @@ -101,6 +101,9 @@ class PathRepositoryTest extends TestCase $package = $packages[0]; $this->assertEquals('test/path-versioned', $package->getName()); - $this->assertEquals(rtrim($relativeUrl, DIRECTORY_SEPARATOR), rtrim($package->getDistUrl(), DIRECTORY_SEPARATOR)); + + // Convert platform specific separators back to generic URL slashes + $relativeUrl = str_replace(DIRECTORY_SEPARATOR, '/', $relativeUrl); + $this->assertEquals(rtrim($relativeUrl, '/'), rtrim($package->getDistUrl(), '/')); } } From adf3b956d094246a461876eb98db9ebf4cb5dedc Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Thu, 21 Jan 2016 13:01:55 +0100 Subject: [PATCH 58/98] try to use unique test directories any tests that use the filesystem should have their own unique directory, as we run our test suite in parallel and cleanup of tests (removing directories) should not interfere with currently running tests --- tests/Composer/Test/AllFunctionalTest.php | 19 +++++++---- .../Test/Autoload/AutoloadGeneratorTest.php | 4 +-- .../Test/Autoload/ClassMapGeneratorTest.php | 15 +++------ tests/Composer/Test/CacheTest.php | 6 ++-- .../Test/Config/JsonConfigSourceTest.php | 6 ++-- .../Test/Downloader/FileDownloaderTest.php | 19 ++++------- .../Test/Downloader/GitDownloaderTest.php | 7 ++-- .../Test/Downloader/HgDownloaderTest.php | 5 +-- .../Downloader/PearPackageExtractorTest.php | 5 +-- .../Downloader/PerforceDownloaderTest.php | 5 +-- .../Test/Downloader/XzDownloaderTest.php | 7 ++-- .../Test/Downloader/ZipDownloaderTest.php | 6 ++-- .../Test/Installer/LibraryInstallerTest.php | 9 +++--- .../Archiver/ArchivableFilesFinderTest.php | 5 +-- .../Test/Package/Archiver/ArchiverTest.php | 6 ++-- .../Package/Archiver/PharArchiverTest.php | 8 ++--- .../Test/Plugin/PluginInstallerTest.php | 2 +- .../Repository/FilesystemRepositoryTest.php | 2 +- .../Test/Repository/Vcs/GitHubDriverTest.php | 9 ++++-- .../Test/Repository/Vcs/GitLabDriverTest.php | 21 +++++++++--- .../Repository/Vcs/PerforceDriverTest.php | 6 ++-- .../Test/Repository/Vcs/SvnDriverTest.php | 32 ++++++++++++++----- .../Test/Repository/VcsRepositoryTest.php | 7 ++-- tests/Composer/Test/Util/FilesystemTest.php | 6 ++-- tests/Composer/TestCase.php | 19 ++++++++++- 25 files changed, 145 insertions(+), 91 deletions(-) diff --git a/tests/Composer/Test/AllFunctionalTest.php b/tests/Composer/Test/AllFunctionalTest.php index df8ddf185..7ef3aa0af 100644 --- a/tests/Composer/Test/AllFunctionalTest.php +++ b/tests/Composer/Test/AllFunctionalTest.php @@ -12,14 +12,15 @@ namespace Composer\Test; -use Symfony\Component\Process\Process; +use Composer\TestCase; use Composer\Util\Filesystem; use Symfony\Component\Finder\Finder; +use Symfony\Component\Process\Process; /** * @group slow */ -class AllFunctionalTest extends \PHPUnit_Framework_TestCase +class AllFunctionalTest extends TestCase { protected $oldcwd; protected $oldenv; @@ -29,17 +30,21 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase public function setUp() { $this->oldcwd = getcwd(); + chdir(__DIR__.'/Fixtures/functional'); } public function tearDown() { chdir($this->oldcwd); + $fs = new Filesystem; + if ($this->testDir) { $fs->removeDirectory($this->testDir); $this->testDir = null; } + if ($this->oldenv) { $fs->removeDirectory(getenv('COMPOSER_HOME')); $_SERVER['COMPOSER_HOME'] = $this->oldenv; @@ -50,7 +55,7 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase public static function setUpBeforeClass() { - self::$pharPath = sys_get_temp_dir().'/composer-phar-test/composer.phar'; + self::$pharPath = self::getUniqueTmpDirectory() . '/composer.phar'; } public static function tearDownAfterClass() @@ -66,9 +71,7 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase } $target = dirname(self::$pharPath); - $fs = new Filesystem; - $fs->removeDirectory($target); - $fs->ensureDirectoryExists($target); + $fs = new Filesystem(); chdir($target); $it = new \RecursiveDirectoryIterator(__DIR__.'/../../../', \RecursiveDirectoryIterator::SKIP_DOTS); @@ -85,9 +88,11 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase $proc = new Process('php '.escapeshellarg('./bin/compile'), $target); $exitcode = $proc->run(); + if ($exitcode !== 0 || trim($proc->getOutput())) { $this->fail($proc->getOutput()); } + $this->assertTrue(file_exists(self::$pharPath)); } @@ -140,7 +145,7 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase $data = array(); $section = null; - $testDir = sys_get_temp_dir().'/composer_functional_test'.uniqid(mt_rand(), true); + $testDir = self::getUniqueTmpDirectory(); $this->testDir = $testDir; $varRegex = '#%([a-zA-Z_-]+)%#'; $variableReplacer = function ($match) use (&$data, $testDir) { diff --git a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php index 502a483ab..07706c5d2 100644 --- a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php +++ b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php @@ -88,8 +88,7 @@ class AutoloadGeneratorTest extends TestCase $this->fs = new Filesystem; $that = $this; - $this->workingDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest-'.md5(uniqid('', true)); - $this->fs->ensureDirectoryExists($this->workingDir); + $this->workingDir = $this->getUniqueTmpDirectory(); $this->vendorDir = $this->workingDir.DIRECTORY_SEPARATOR.'composer-test-autoload'; $this->ensureDirectoryExistsAndClear($this->vendorDir); @@ -144,6 +143,7 @@ class AutoloadGeneratorTest extends TestCase if (is_dir($this->workingDir)) { $this->fs->removeDirectory($this->workingDir); } + if (is_dir($this->vendorDir)) { $this->fs->removeDirectory($this->vendorDir); } diff --git a/tests/Composer/Test/Autoload/ClassMapGeneratorTest.php b/tests/Composer/Test/Autoload/ClassMapGeneratorTest.php index 3b703d8f3..cd3d43260 100644 --- a/tests/Composer/Test/Autoload/ClassMapGeneratorTest.php +++ b/tests/Composer/Test/Autoload/ClassMapGeneratorTest.php @@ -19,10 +19,11 @@ namespace Composer\Test\Autoload; use Composer\Autoload\ClassMapGenerator; +use Composer\TestCase; use Symfony\Component\Finder\Finder; use Composer\Util\Filesystem; -class ClassMapGeneratorTest extends \PHPUnit_Framework_TestCase +class ClassMapGeneratorTest extends TestCase { /** * @dataProvider getTestCreateMapTests @@ -127,10 +128,8 @@ class ClassMapGeneratorTest extends \PHPUnit_Framework_TestCase { $this->checkIfFinderIsAvailable(); - $tempDir = sys_get_temp_dir().'/ComposerTestAmbiguousRefs'; - if (!is_dir($tempDir.'/other')) { - mkdir($tempDir.'/other', 0777, true); - } + $tempDir = $this->getUniqueTmpDirectory(); + $this->ensureDirectoryExistsAndClear($tempDir.'/other'); $finder = new Finder(); $finder->files()->in($tempDir); @@ -171,13 +170,9 @@ class ClassMapGeneratorTest extends \PHPUnit_Framework_TestCase */ public function testUnambiguousReference() { - $tempDir = sys_get_temp_dir().'/ComposerTestUnambiguousRefs'; - if (!is_dir($tempDir)) { - mkdir($tempDir, 0777, true); - } + $tempDir = $this->getUniqueTmpDirectory(); file_put_contents($tempDir.'/A.php', "markTestSkipped('Test causes intermittent failures on Travis'); } - $this->root = sys_get_temp_dir() . '/composer_testdir'; - $this->ensureDirectoryExistsAndClear($this->root); - + $this->root = $this->getUniqueTmpDirectory(); $this->files = array(); $zeros = str_repeat('0', 1000); + for ($i = 0; $i < 4; $i++) { file_put_contents("{$this->root}/cached.file{$i}.zip", $zeros); $this->files[] = new \SplFileInfo("{$this->root}/cached.file{$i}.zip"); } + $this->finder = $this->getMockBuilder('Symfony\Component\Finder\Finder')->disableOriginalConstructor()->getMock(); $io = $this->getMock('Composer\IO\IOInterface'); diff --git a/tests/Composer/Test/Config/JsonConfigSourceTest.php b/tests/Composer/Test/Config/JsonConfigSourceTest.php index 529532263..0ea487076 100644 --- a/tests/Composer/Test/Config/JsonConfigSourceTest.php +++ b/tests/Composer/Test/Config/JsonConfigSourceTest.php @@ -14,9 +14,10 @@ namespace Composer\Test\Json; use Composer\Config\JsonConfigSource; use Composer\Json\JsonFile; +use Composer\TestCase; use Composer\Util\Filesystem; -class JsonConfigSourceTest extends \PHPUnit_Framework_TestCase +class JsonConfigSourceTest extends TestCase { /** @var Filesystem */ private $fs; @@ -31,8 +32,7 @@ class JsonConfigSourceTest extends \PHPUnit_Framework_TestCase protected function setUp() { $this->fs = new Filesystem; - $this->workingDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest'; - $this->fs->ensureDirectoryExists($this->workingDir); + $this->workingDir = $this->getUniqueTmpDirectory(); } protected function tearDown() diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php index f0578f6be..9b9f7b671 100644 --- a/tests/Composer/Test/Downloader/FileDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -13,9 +13,10 @@ namespace Composer\Test\Downloader; use Composer\Downloader\FileDownloader; +use Composer\TestCase; use Composer\Util\Filesystem; -class FileDownloaderTest extends \PHPUnit_Framework_TestCase +class FileDownloaderTest extends TestCase { protected function getDownloader($io = null, $config = null, $eventDispatcher = null, $cache = null, $rfs = null, $filesystem = null) { @@ -53,9 +54,9 @@ class FileDownloaderTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue(array('url'))) ; - $path = tempnam(sys_get_temp_dir(), 'c'); - + $path = tempnam($this->getUniqueTmpDirectory(), 'c'); $downloader = $this->getDownloader(); + try { $downloader->download($packageMock, $path); $this->fail(); @@ -102,10 +103,7 @@ class FileDownloaderTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue(array())) ; - do { - $path = sys_get_temp_dir().'/'.md5(time().mt_rand()); - } while (file_exists($path)); - + $path = $this->getUniqueTmpDirectory(); $ioMock = $this->getMock('Composer\IO\IOInterface'); $ioMock->expects($this->any()) ->method('write') @@ -187,14 +185,9 @@ class FileDownloaderTest extends \PHPUnit_Framework_TestCase ; $filesystem = $this->getMock('Composer\Util\Filesystem'); - do { - $path = sys_get_temp_dir().'/'.md5(time().mt_rand()); - } while (file_exists($path)); - + $path = $this->getUniqueTmpDirectory(); $downloader = $this->getDownloader(null, null, null, null, null, $filesystem); - // make sure the file expected to be downloaded is on disk already - mkdir($path, 0777, true); touch($path.'/script.js'); try { diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index f0b6699e5..26437ada5 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -14,9 +14,10 @@ namespace Composer\Test\Downloader; use Composer\Downloader\GitDownloader; use Composer\Config; +use Composer\TestCase; use Composer\Util\Filesystem; -class GitDownloaderTest extends \PHPUnit_Framework_TestCase +class GitDownloaderTest extends TestCase { /** @var Filesystem */ private $fs; @@ -26,7 +27,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase protected function setUp() { $this->fs = new Filesystem; - $this->workingDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest-'.md5(uniqid('', true)); + $this->workingDir = $this->getUniqueTmpDirectory(); } protected function tearDown() @@ -317,7 +318,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase ->method('execute') ->with($this->equalTo($expectedGitUpdateCommand)) ->will($this->returnValue(1)); - + $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); $downloader->update($packageMock, $packageMock, $this->workingDir); diff --git a/tests/Composer/Test/Downloader/HgDownloaderTest.php b/tests/Composer/Test/Downloader/HgDownloaderTest.php index b2dee627b..75dfa84aa 100644 --- a/tests/Composer/Test/Downloader/HgDownloaderTest.php +++ b/tests/Composer/Test/Downloader/HgDownloaderTest.php @@ -13,16 +13,17 @@ namespace Composer\Test\Downloader; use Composer\Downloader\HgDownloader; +use Composer\TestCase; use Composer\Util\Filesystem; -class HgDownloaderTest extends \PHPUnit_Framework_TestCase +class HgDownloaderTest extends TestCase { /** @var string */ private $workingDir; protected function setUp() { - $this->workingDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest-'.md5(uniqid('', true)); + $this->workingDir = $this->getUniqueTmpDirectory(); } protected function tearDown() diff --git a/tests/Composer/Test/Downloader/PearPackageExtractorTest.php b/tests/Composer/Test/Downloader/PearPackageExtractorTest.php index 10ac27955..92004d0f1 100644 --- a/tests/Composer/Test/Downloader/PearPackageExtractorTest.php +++ b/tests/Composer/Test/Downloader/PearPackageExtractorTest.php @@ -13,8 +13,9 @@ namespace Composer\Test\Downloader; use Composer\Downloader\PearPackageExtractor; +use Composer\TestCase; -class PearPackageExtractorTest extends \PHPUnit_Framework_TestCase +class PearPackageExtractorTest extends TestCase { public function testShouldExtractPackage_1_0() { @@ -122,7 +123,7 @@ class PearPackageExtractorTest extends \PHPUnit_Framework_TestCase public function testShouldPerformReplacements() { - $from = tempnam(sys_get_temp_dir(), 'pear-extract'); + $from = tempnam($this->getUniqueTmpDirectory(), 'pear-extract'); $to = $from.'-to'; $original = 'replaced: @placeholder@; not replaced: @another@; replaced again: @placeholder@'; diff --git a/tests/Composer/Test/Downloader/PerforceDownloaderTest.php b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php index fc4297633..2b8105e65 100644 --- a/tests/Composer/Test/Downloader/PerforceDownloaderTest.php +++ b/tests/Composer/Test/Downloader/PerforceDownloaderTest.php @@ -16,12 +16,13 @@ use Composer\Downloader\PerforceDownloader; use Composer\Config; use Composer\Repository\VcsRepository; use Composer\IO\IOInterface; +use Composer\TestCase; use Composer\Util\Filesystem; /** * @author Matt Whittom */ -class PerforceDownloaderTest extends \PHPUnit_Framework_TestCase +class PerforceDownloaderTest extends TestCase { protected $config; protected $downloader; @@ -34,7 +35,7 @@ class PerforceDownloaderTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->testPath = sys_get_temp_dir() . '/composer-test'; + $this->testPath = $this->getUniqueTmpDirectory(); $this->repoConfig = $this->getRepoConfig(); $this->config = $this->getConfig(); $this->io = $this->getMockIoInterface(); diff --git a/tests/Composer/Test/Downloader/XzDownloaderTest.php b/tests/Composer/Test/Downloader/XzDownloaderTest.php index a71516821..418776d75 100644 --- a/tests/Composer/Test/Downloader/XzDownloaderTest.php +++ b/tests/Composer/Test/Downloader/XzDownloaderTest.php @@ -13,10 +13,11 @@ namespace Composer\Test\Downloader; use Composer\Downloader\XzDownloader; +use Composer\TestCase; use Composer\Util\Filesystem; use Composer\Util\RemoteFilesystem; -class XzDownloaderTest extends \PHPUnit_Framework_TestCase +class XzDownloaderTest extends TestCase { /** * @var Filesystem @@ -33,7 +34,7 @@ class XzDownloaderTest extends \PHPUnit_Framework_TestCase if (defined('PHP_WINDOWS_VERSION_BUILD')) { $this->markTestSkipped('Skip test on Windows'); } - $this->testDir = sys_get_temp_dir().'/composer-xz-test-vendor'; + $this->testDir = $this->getUniqueTmpDirectory(); } public function tearDown() @@ -67,7 +68,7 @@ class XzDownloaderTest extends \PHPUnit_Framework_TestCase $downloader = new XzDownloader($io, $config, null, null, null, new RemoteFilesystem($io)); try { - $downloader->download($packageMock, sys_get_temp_dir().'/composer-xz-test'); + $downloader->download($packageMock, $this->getUniqueTmpDirectory()); $this->fail('Download of invalid tarball should throw an exception'); } catch (\RuntimeException $e) { $this->assertContains('File format not recognized', $e->getMessage()); diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index cb5a56569..1eda038fb 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -13,9 +13,10 @@ namespace Composer\Test\Downloader; use Composer\Downloader\ZipDownloader; +use Composer\TestCase; use Composer\Util\Filesystem; -class ZipDownloaderTest extends \PHPUnit_Framework_TestCase +class ZipDownloaderTest extends TestCase { /** @@ -28,7 +29,8 @@ class ZipDownloaderTest extends \PHPUnit_Framework_TestCase if (!class_exists('ZipArchive')) { $this->markTestSkipped('zip extension missing'); } - $this->testDir = sys_get_temp_dir().'/composer-zip-test-vendor'; + + $this->testDir = $this->getUniqueTmpDirectory(); } public function tearDown() diff --git a/tests/Composer/Test/Installer/LibraryInstallerTest.php b/tests/Composer/Test/Installer/LibraryInstallerTest.php index 6230752e5..720b2da40 100644 --- a/tests/Composer/Test/Installer/LibraryInstallerTest.php +++ b/tests/Composer/Test/Installer/LibraryInstallerTest.php @@ -22,6 +22,7 @@ class LibraryInstallerTest extends TestCase { protected $composer; protected $config; + protected $rootDir; protected $vendorDir; protected $binDir; protected $dm; @@ -37,10 +38,11 @@ class LibraryInstallerTest extends TestCase $this->config = new Config(); $this->composer->setConfig($this->config); - $this->vendorDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'composer-test-vendor'; + $this->rootDir = $this->getUniqueTmpDirectory(); + $this->vendorDir = $this->rootDir.'/vendor'; $this->ensureDirectoryExistsAndClear($this->vendorDir); - $this->binDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'composer-test-bin'; + $this->binDir = $this->rootDir.'/bin'; $this->ensureDirectoryExistsAndClear($this->binDir); $this->config->merge(array( @@ -61,8 +63,7 @@ class LibraryInstallerTest extends TestCase protected function tearDown() { - $this->fs->removeDirectory($this->vendorDir); - $this->fs->removeDirectory($this->binDir); + $this->fs->removeDirectory($this->rootDir); } public function testInstallerCreationShouldNotCreateVendorDirectory() diff --git a/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php b/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php index f395eba6e..cce67c1aa 100644 --- a/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php @@ -13,11 +13,12 @@ namespace Composer\Test\Package\Archiver; use Composer\Package\Archiver\ArchivableFilesFinder; +use Composer\TestCase; use Composer\Util\Filesystem; use Symfony\Component\Process\Process; use Symfony\Component\Process\ExecutableFinder; -class ArchivableFilesFinderTest extends \PHPUnit_Framework_TestCase +class ArchivableFilesFinderTest extends TestCase { protected $sources; protected $finder; @@ -29,7 +30,7 @@ class ArchivableFilesFinderTest extends \PHPUnit_Framework_TestCase $this->fs = $fs; $this->sources = $fs->normalizePath( - realpath(sys_get_temp_dir()).'/composer_archiver_test'.uniqid(mt_rand(), true) + $this->getUniqueTmpDirectory() ); $fileTree = array( diff --git a/tests/Composer/Test/Package/Archiver/ArchiverTest.php b/tests/Composer/Test/Package/Archiver/ArchiverTest.php index a3c73fa7a..32a6ed749 100644 --- a/tests/Composer/Test/Package/Archiver/ArchiverTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchiverTest.php @@ -12,11 +12,12 @@ namespace Composer\Test\Package\Archiver; +use Composer\TestCase; use Composer\Util\Filesystem; use Composer\Util\ProcessExecutor; use Composer\Package\Package; -abstract class ArchiverTest extends \PHPUnit_Framework_TestCase +abstract class ArchiverTest extends TestCase { /** * @var \Composer\Util\Filesystem @@ -37,8 +38,7 @@ abstract class ArchiverTest extends \PHPUnit_Framework_TestCase { $this->filesystem = new Filesystem(); $this->process = new ProcessExecutor(); - $this->testDir = sys_get_temp_dir().'/composer_archiver_test_'.mt_rand(); - $this->filesystem->ensureDirectoryExists($this->testDir); + $this->testDir = $this->getUniqueTmpDirectory(); } public function tearDown() diff --git a/tests/Composer/Test/Package/Archiver/PharArchiverTest.php b/tests/Composer/Test/Package/Archiver/PharArchiverTest.php index d6e783c91..16753784d 100644 --- a/tests/Composer/Test/Package/Archiver/PharArchiverTest.php +++ b/tests/Composer/Test/Package/Archiver/PharArchiverTest.php @@ -21,14 +21,14 @@ class PharArchiverTest extends ArchiverTest // Set up repository $this->setupDummyRepo(); $package = $this->setupPackage(); - $target = sys_get_temp_dir().'/composer_archiver_test.tar'; + $target = $this->getUniqueTmpDirectory().'/composer_archiver_test.tar'; // Test archive $archiver = new PharArchiver(); $archiver->archive($package->getSourceUrl(), $target, 'tar', array('foo/bar', 'baz', '!/foo/bar/baz')); $this->assertFileExists($target); - unlink($target); + $this->filesystem->removeDirectory(dirname($target)); } public function testZipArchive() @@ -36,14 +36,14 @@ class PharArchiverTest extends ArchiverTest // Set up repository $this->setupDummyRepo(); $package = $this->setupPackage(); - $target = sys_get_temp_dir().'/composer_archiver_test.zip'; + $target = $this->getUniqueTmpDirectory().'/composer_archiver_test.zip'; // Test archive $archiver = new PharArchiver(); $archiver->archive($package->getSourceUrl(), $target, 'zip'); $this->assertFileExists($target); - unlink($target); + $this->filesystem->removeDirectory(dirname($target)); } /** diff --git a/tests/Composer/Test/Plugin/PluginInstallerTest.php b/tests/Composer/Test/Plugin/PluginInstallerTest.php index b449d7e90..db1a64579 100644 --- a/tests/Composer/Test/Plugin/PluginInstallerTest.php +++ b/tests/Composer/Test/Plugin/PluginInstallerTest.php @@ -69,7 +69,7 @@ class PluginInstallerTest extends TestCase { $loader = new JsonLoader(new ArrayLoader()); $this->packages = array(); - $this->directory = sys_get_temp_dir() . '/' . uniqid(); + $this->directory = $this->getUniqueTmpDirectory(); for ($i = 1; $i <= 7; $i++) { $filename = '/Fixtures/plugin-v'.$i.'/composer.json'; mkdir(dirname($this->directory . $filename), 0777, true); diff --git a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php index 6f8b71d20..cde5eb402 100644 --- a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php +++ b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php @@ -42,7 +42,7 @@ class FilesystemRepositoryTest extends TestCase } /** - * @expectedException Composer\Repository\InvalidRepositoryException + * @expectedException \Composer\Repository\InvalidRepositoryException */ public function testCorruptedRepositoryFile() { diff --git a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php index cd40a71f2..ee7ad38fd 100644 --- a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php @@ -14,19 +14,22 @@ namespace Composer\Test\Repository\Vcs; use Composer\Downloader\TransportException; use Composer\Repository\Vcs\GitHubDriver; +use Composer\TestCase; use Composer\Util\Filesystem; use Composer\Config; -class GitHubDriverTest extends \PHPUnit_Framework_TestCase +class GitHubDriverTest extends TestCase { + private $home; private $config; public function setUp() { + $this->home = $this->getUniqueTmpDirectory(); $this->config = new Config(); $this->config->merge(array( 'config' => array( - 'home' => sys_get_temp_dir() . '/composer-test', + 'home' => $this->home, ), )); } @@ -34,7 +37,7 @@ class GitHubDriverTest extends \PHPUnit_Framework_TestCase public function tearDown() { $fs = new Filesystem; - $fs->removeDirectory(sys_get_temp_dir() . '/composer-test'); + $fs->removeDirectory($this->home); } public function testPrivateRepository() diff --git a/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php index dc08b9aa8..e1ac82021 100644 --- a/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php @@ -14,29 +14,42 @@ namespace Composer\Test\Repository\Vcs; use Composer\Repository\Vcs\GitLabDriver; use Composer\Config; +use Composer\TestCase; +use Composer\Util\Filesystem; /** * @author Jérôme Tamarelle */ -class GitLabDriverTest extends \PHPUnit_Framework_TestCase +class GitLabDriverTest extends TestCase { + private $home; + private $config; + private $io; + private $process; + private $remoteFilesystem; + public function setUp() { + $this->home = $this->getUniqueTmpDirectory(); $this->config = new Config(); $this->config->merge(array( 'config' => array( - 'home' => sys_get_temp_dir().'/composer-test', + 'home' => $this->home, 'gitlab-domains' => array('mycompany.com/gitlab', 'gitlab.com') ), )); $this->io = $this->prophesize('Composer\IO\IOInterface'); - $this->process = $this->prophesize('Composer\Util\ProcessExecutor'); - $this->remoteFilesystem = $this->prophesize('Composer\Util\RemoteFilesystem'); } + public function tearDown() + { + $fs = new Filesystem(); + $fs->removeDirectory($this->home); + } + public function getInitializeUrls() { return array( diff --git a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php index 59030f506..987751408 100644 --- a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php @@ -13,6 +13,7 @@ namespace Composer\Test\Repository\Vcs; use Composer\Repository\Vcs\PerforceDriver; +use Composer\TestCase; use Composer\Util\Filesystem; use Composer\Config; use Composer\Util\Perforce; @@ -20,7 +21,7 @@ use Composer\Util\Perforce; /** * @author Matt Whittom */ -class PerforceDriverTest extends \PHPUnit_Framework_TestCase +class PerforceDriverTest extends TestCase { protected $config; protected $io; @@ -29,6 +30,7 @@ class PerforceDriverTest extends \PHPUnit_Framework_TestCase protected $testPath; protected $driver; protected $repoConfig; + protected $perforce; const TEST_URL = 'TEST_PERFORCE_URL'; const TEST_DEPOT = 'TEST_DEPOT_CONFIG'; @@ -36,7 +38,7 @@ class PerforceDriverTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->testPath = sys_get_temp_dir() . '/composer-test'; + $this->testPath = $this->getUniqueTmpDirectory(); $this->config = $this->getTestConfig($this->testPath); $this->repoConfig = $this->getTestRepoConfig(); $this->io = $this->getMockIOInterface(); diff --git a/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php b/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php index 2ef1baa18..c2ae497ca 100644 --- a/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php @@ -14,9 +14,31 @@ namespace Composer\Test\Repository\Vcs; use Composer\Repository\Vcs\SvnDriver; use Composer\Config; +use Composer\TestCase; +use Composer\Util\Filesystem; -class SvnDriverTest extends \PHPUnit_Framework_TestCase +class SvnDriverTest extends TestCase { + protected $home; + protected $config; + + public function setUp() + { + $this->home = $this->getUniqueTmpDirectory(); + $this->config = new Config(); + $this->config->merge(array( + 'config' => array( + 'home' => $this->home, + ), + )); + } + + public function tearDown() + { + $fs = new Filesystem(); + $fs->removeDirectory($this->home); + } + /** * @expectedException RuntimeException */ @@ -39,17 +61,11 @@ class SvnDriverTest extends \PHPUnit_Framework_TestCase ->method('execute') ->will($this->returnValue(0)); - $config = new Config(); - $config->merge(array( - 'config' => array( - 'home' => sys_get_temp_dir() . '/composer-test', - ), - )); $repoConfig = array( 'url' => 'http://till:secret@corp.svn.local/repo', ); - $svn = new SvnDriver($repoConfig, $console, $config, $process); + $svn = new SvnDriver($repoConfig, $console, $this->config, $process); $svn->initialize(); } diff --git a/tests/Composer/Test/Repository/VcsRepositoryTest.php b/tests/Composer/Test/Repository/VcsRepositoryTest.php index eaedc82a9..61e29be37 100644 --- a/tests/Composer/Test/Repository/VcsRepositoryTest.php +++ b/tests/Composer/Test/Repository/VcsRepositoryTest.php @@ -12,6 +12,7 @@ namespace Composer\Test\Repository; +use Composer\TestCase; use Symfony\Component\Process\ExecutableFinder; use Composer\Package\Dumper\ArrayDumper; use Composer\Repository\VcsRepository; @@ -23,7 +24,7 @@ use Composer\Config; /** * @group slow */ -class VcsRepositoryTest extends \PHPUnit_Framework_TestCase +class VcsRepositoryTest extends TestCase { private static $composerHome; private static $gitRepo; @@ -32,8 +33,8 @@ class VcsRepositoryTest extends \PHPUnit_Framework_TestCase protected function initialize() { $oldCwd = getcwd(); - self::$composerHome = sys_get_temp_dir() . '/composer-home-'.mt_rand().'/'; - self::$gitRepo = sys_get_temp_dir() . '/composer-git-'.mt_rand().'/'; + self::$composerHome = $this->getUniqueTmpDirectory(); + self::$gitRepo = $this->getUniqueTmpDirectory(); $locator = new ExecutableFinder(); if (!$locator->find('git')) { diff --git a/tests/Composer/Test/Util/FilesystemTest.php b/tests/Composer/Test/Util/FilesystemTest.php index d7c986369..969572036 100644 --- a/tests/Composer/Test/Util/FilesystemTest.php +++ b/tests/Composer/Test/Util/FilesystemTest.php @@ -44,8 +44,8 @@ class FilesystemTest extends TestCase public function setUp() { $this->fs = new Filesystem; - $this->workingDir = sys_get_temp_dir() . '/composer_testdir'; - $this->testFile = sys_get_temp_dir() . '/composer_test_file'; + $this->workingDir = $this->getUniqueTmpDirectory(); + $this->testFile = $this->getUniqueTmpDirectory() . '/composer_test_file'; } public function tearDown() @@ -54,7 +54,7 @@ class FilesystemTest extends TestCase $this->fs->removeDirectory($this->workingDir); } if (is_file($this->testFile)) { - $this->fs->remove($this->testFile); + $this->fs->removeDirectory(dirname($this->testFile)); } } diff --git a/tests/Composer/TestCase.php b/tests/Composer/TestCase.php index 2057c09b8..4e115f9b0 100644 --- a/tests/Composer/TestCase.php +++ b/tests/Composer/TestCase.php @@ -56,12 +56,29 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase return new AliasPackage($package, $normVersion, $version); } - protected function ensureDirectoryExistsAndClear($directory) + protected static function getUniqueTmpDirectory() + { + $attempts = 5; + $root = sys_get_temp_dir(); + + do { + $unique = $root . DIRECTORY_SEPARATOR . uniqid('composer-test-'); + if (!file_exists($unique) && mkdir($unique, 0777)) { + return $unique; + } + } while (--$attempts); + + throw new \RuntimeException('Failed to create a unique temporary directory.'); + } + + protected static function ensureDirectoryExistsAndClear($directory) { $fs = new Filesystem(); + if (is_dir($directory)) { $fs->removeDirectory($directory); } + mkdir($directory, 0777, true); } } From 5e73b21c706e01bc98feddaf00281c67bf610886 Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Tue, 26 Jan 2016 09:40:33 +0100 Subject: [PATCH 59/98] return realpath() value (OSX uses a weird symlink structure) --- tests/Composer/TestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Composer/TestCase.php b/tests/Composer/TestCase.php index 4e115f9b0..a065f200c 100644 --- a/tests/Composer/TestCase.php +++ b/tests/Composer/TestCase.php @@ -64,7 +64,7 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase do { $unique = $root . DIRECTORY_SEPARATOR . uniqid('composer-test-'); if (!file_exists($unique) && mkdir($unique, 0777)) { - return $unique; + return realpath($unique); } } while (--$attempts); From a8995b25727c72fc7a3a267bf382c5c8c8741151 Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Tue, 26 Jan 2016 11:23:08 +0100 Subject: [PATCH 60/98] use dirsep so phpunit on windows doesnt fail --- tests/Composer/Test/Installer/LibraryInstallerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Composer/Test/Installer/LibraryInstallerTest.php b/tests/Composer/Test/Installer/LibraryInstallerTest.php index 720b2da40..72eeb04b1 100644 --- a/tests/Composer/Test/Installer/LibraryInstallerTest.php +++ b/tests/Composer/Test/Installer/LibraryInstallerTest.php @@ -39,10 +39,10 @@ class LibraryInstallerTest extends TestCase $this->composer->setConfig($this->config); $this->rootDir = $this->getUniqueTmpDirectory(); - $this->vendorDir = $this->rootDir.'/vendor'; + $this->vendorDir = $this->rootDir.DIRECTORY_SEPARATOR.'vendor'; $this->ensureDirectoryExistsAndClear($this->vendorDir); - $this->binDir = $this->rootDir.'/bin'; + $this->binDir = $this->rootDir.DIRECTORY_SEPARATOR.'bin'; $this->ensureDirectoryExistsAndClear($this->binDir); $this->config->merge(array( From fff5074bbf5697a907b2832902d4219801b90bc5 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 26 Jan 2016 10:43:59 +0000 Subject: [PATCH 61/98] Fix additionalOptions getting dropped when SAN and redirect handling are combined, refs #4782 --- src/Composer/Util/RemoteFilesystem.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index b934bd145..0f906f67a 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -208,21 +208,23 @@ class RemoteFilesystem $this->io->setAuthentication($originUrl, urldecode($match[1]), urldecode($match[2])); } - if (isset($additionalOptions['retry-auth-failure'])) { - $this->retryAuthFailure = (bool) $additionalOptions['retry-auth-failure']; + $tempAdditionalOptions = $additionalOptions; + if (isset($tempAdditionalOptions['retry-auth-failure'])) { + $this->retryAuthFailure = (bool) $tempAdditionalOptions['retry-auth-failure']; - unset($additionalOptions['retry-auth-failure']); + unset($tempAdditionalOptions['retry-auth-failure']); } $isRedirect = false; - if (isset($additionalOptions['redirects'])) { - $this->redirects = $additionalOptions['redirects']; + if (isset($tempAdditionalOptions['redirects'])) { + $this->redirects = $tempAdditionalOptions['redirects']; $isRedirect = true; - unset($additionalOptions['redirects']); + unset($tempAdditionalOptions['redirects']); } - $options = $this->getOptionsForUrl($originUrl, $additionalOptions); + $options = $this->getOptionsForUrl($originUrl, $tempAdditionalOptions); + unset($tempAdditionalOptions); $userlandFollow = isset($options['http']['follow_location']) && !$options['http']['follow_location']; if ($this->io->isDebug()) { @@ -422,7 +424,7 @@ class RemoteFilesystem $result = $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress); - if (false !== $this->storeAuth) { + if ($this->storeAuth && $this->config) { $authHelper = new AuthHelper($this->io, $this->config); $authHelper->storeAuth($this->originUrl, $this->storeAuth); $this->storeAuth = false; From 41937ef781ee84103ab64ce042d9fbdf4501319e Mon Sep 17 00:00:00 2001 From: Daniel Mason Date: Tue, 26 Jan 2016 11:38:12 +0000 Subject: [PATCH 62/98] More comprehensive documentation of event handlers --- doc/articles/plugins.md | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/doc/articles/plugins.md b/doc/articles/plugins.md index edaa013f0..645ae51c0 100644 --- a/doc/articles/plugins.md +++ b/doc/articles/plugins.md @@ -89,9 +89,45 @@ Furthermore plugins may implement the event handlers automatically registered with the `EventDispatcher` when the plugin is loaded. -Plugin can subscribe to any of the available [script events](scripts.md#event-names). +To register a method to an event, implement the method `getSubscribedEvents()` and have it return an array. The array key must be the event name ([listed here](https://getcomposer.org/doc/articles/scripts.md#event-names)) and the value is the name of the method in this class to be called. -Example: +```php +public static function getSubscribedEvents() +{ + return array( + 'post-autoload-dump' => 'methodToBeCalled', + // ^ event name ^ ^ method name ^ + ); +} +``` + +By default, the priority of an event handler is set to 0. The priorty can be changed by attaching a tuple where the first value is the method name, as before, and the second value is an integer representing the priority. Higher integers represent higher priorityes therefore, priortity 2 is called before priority 1, etc. + +```php +public static function getSubscribedEvents() +{ + return array( + // Will be called before events with priority 0 + 'post-autoload-dump' => array('methodToBeCalled', 1) + ); +} +``` + +If multiple methods should be called, then an array of tupples can be attached to each event. The tupples do not need to include the priority. If it is omitted, it will default to 0. + +```php +public static function getSubscribedEvents() +{ + return [ + 'post-autoload-dump' => array( + array('methodToBeCalled' ), // Priority defaults to 0 + array('someOtherMethodName', 1), // This fires first + ) + ]; +} +``` + +Here's a complete example: ```php Date: Tue, 26 Jan 2016 11:47:36 +0000 Subject: [PATCH 63/98] Removed new style array syntax --- doc/articles/plugins.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/articles/plugins.md b/doc/articles/plugins.md index 645ae51c0..424f08605 100644 --- a/doc/articles/plugins.md +++ b/doc/articles/plugins.md @@ -118,12 +118,12 @@ If multiple methods should be called, then an array of tupples can be attached t ```php public static function getSubscribedEvents() { - return [ + return array( 'post-autoload-dump' => array( array('methodToBeCalled' ), // Priority defaults to 0 array('someOtherMethodName', 1), // This fires first ) - ]; + ); } ``` From c4df3ec0d4394e7cd4e55b6b076b62888d05c3d2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 26 Jan 2016 12:17:10 +0000 Subject: [PATCH 64/98] Rewrap text and few minor typos, refs #4832 --- doc/articles/plugins.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/doc/articles/plugins.md b/doc/articles/plugins.md index 424f08605..b4997d22d 100644 --- a/doc/articles/plugins.md +++ b/doc/articles/plugins.md @@ -36,7 +36,7 @@ as a normal package's. The current composer plugin API version is 1.0.0. -An example of a valid plugin `composer.json` file (with the autoloading +An example of a valid plugin `composer.json` file (with the autoloading part omitted): ```json @@ -89,31 +89,40 @@ Furthermore plugins may implement the event handlers automatically registered with the `EventDispatcher` when the plugin is loaded. -To register a method to an event, implement the method `getSubscribedEvents()` and have it return an array. The array key must be the event name ([listed here](https://getcomposer.org/doc/articles/scripts.md#event-names)) and the value is the name of the method in this class to be called. +To register a method to an event, implement the method `getSubscribedEvents()` +and have it return an array. The array key must be the +[event name](https://getcomposer.org/doc/articles/scripts.md#event-names) +and the value is the name of the method in this class to be called. ```php public static function getSubscribedEvents() { return array( 'post-autoload-dump' => 'methodToBeCalled', - // ^ event name ^ ^ method name ^ + // ^ event name ^ ^ method name ^ ); } ``` -By default, the priority of an event handler is set to 0. The priorty can be changed by attaching a tuple where the first value is the method name, as before, and the second value is an integer representing the priority. Higher integers represent higher priorityes therefore, priortity 2 is called before priority 1, etc. +By default, the priority of an event handler is set to 0. The priorty can be +changed by attaching a tuple where the first value is the method name, as +before, and the second value is an integer representing the priority. +Higher integers represent higher priorities. Priortity 2 is called before +priority 1, etc. ```php public static function getSubscribedEvents() { return array( // Will be called before events with priority 0 - 'post-autoload-dump' => array('methodToBeCalled', 1) + 'post-autoload-dump' => array('methodToBeCalled', 1) ); } ``` -If multiple methods should be called, then an array of tupples can be attached to each event. The tupples do not need to include the priority. If it is omitted, it will default to 0. +If multiple methods should be called, then an array of tuples can be attached +to each event. The tuples do not need to include the priority. If it is +omitted, it will default to 0. ```php public static function getSubscribedEvents() From c2e768b8ad8d03e2076dba48ea52d52bd60238f1 Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Tue, 26 Jan 2016 13:27:24 +0100 Subject: [PATCH 65/98] Made Silencer invocations safer against exceptions. --- src/Composer/Command/ConfigCommand.php | 6 ++---- src/Composer/Command/CreateProjectCommand.php | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index 7f4fde6d4..87fdc4ffb 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -159,18 +159,16 @@ EOT $this->authConfigSource = new JsonConfigSource($this->authConfigFile, true); // Initialize the global file if it's not there, ignoring any warnings or notices - Silencer::suppress(); if ($input->getOption('global') && !$this->configFile->exists()) { touch($this->configFile->getPath()); $this->configFile->write(array('config' => new \ArrayObject)); - chmod($this->configFile->getPath(), 0600); + Silencer::call('chmod', $this->configFile->getPath(), 0600); } if ($input->getOption('global') && !$this->authConfigFile->exists()) { touch($this->authConfigFile->getPath()); $this->authConfigFile->write(array('http-basic' => new \ArrayObject, 'github-oauth' => new \ArrayObject, 'gitlab-oauth' => new \ArrayObject)); - chmod($this->authConfigFile->getPath(), 0600); + Silencer::call('chmod', $this->authConfigFile->getPath(), 0600); } - Silencer::restore(); if (!$this->configFile->exists()) { throw new \RuntimeException(sprintf('File "%s" cannot be found in the current directory', $configFile)); diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index df50c4750..dbb7f6802 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -225,13 +225,11 @@ EOT chdir($oldCwd); $vendorComposerDir = $composer->getConfig()->get('vendor-dir').'/composer'; if (is_dir($vendorComposerDir) && $fs->isDirEmpty($vendorComposerDir)) { - Silencer::suppress(); - rmdir($vendorComposerDir); + Silencer::call('rmdir', $vendorComposerDir); $vendorDir = $composer->getConfig()->get('vendor-dir'); if (is_dir($vendorDir) && $fs->isDirEmpty($vendorDir)) { - rmdir($vendorDir); + Silencer::call('rmdir', $vendorDir); } - Silencer::restore(); } return 0; From 64d653ad92c5e3f28fbc0a51013f7feef7f74b15 Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Tue, 26 Jan 2016 13:54:35 +0100 Subject: [PATCH 66/98] fix race condition --- tests/Composer/TestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Composer/TestCase.php b/tests/Composer/TestCase.php index a065f200c..299ac9903 100644 --- a/tests/Composer/TestCase.php +++ b/tests/Composer/TestCase.php @@ -63,7 +63,7 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase do { $unique = $root . DIRECTORY_SEPARATOR . uniqid('composer-test-'); - if (!file_exists($unique) && mkdir($unique, 0777)) { + if (!file_exists($unique) && false !== @mkdir($unique, 0777)) { return realpath($unique); } } while (--$attempts); From 1818b95149c81ea5bcc47eda4944c01acc3f1480 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 26 Jan 2016 13:07:05 +0000 Subject: [PATCH 67/98] CS fixes --- src/Composer/Command/ConfigCommand.php | 4 +-- src/Composer/Command/CreateProjectCommand.php | 1 - src/Composer/Command/DependsCommand.php | 2 +- src/Composer/Command/SelfUpdateCommand.php | 2 +- src/Composer/Command/ShowCommand.php | 26 +++++++++---------- src/Composer/Console/Application.php | 1 - .../SolverProblemsException.php | 2 +- src/Composer/Factory.php | 16 ++++++------ src/Composer/Package/AliasPackage.php | 2 +- src/Composer/Plugin/PluginManager.php | 17 ++++++------ .../Repository/ComposerRepository.php | 1 + src/Composer/Repository/PathRepository.php | 2 +- .../Repository/PlatformRepository.php | 1 + src/Composer/Repository/RepositoryManager.php | 1 - src/Composer/Util/ConfigValidator.php | 1 - src/Composer/Util/Keys.php | 6 ++--- src/Composer/Util/RemoteFilesystem.php | 12 ++++----- src/Composer/Util/Silencer.php | 20 ++++++++------ src/Composer/Util/TlsHelper.php | 8 +++--- .../Test/Config/JsonConfigSourceTest.php | 6 ++--- tests/Composer/Test/DefaultConfigTest.php | 3 +-- .../Test/Downloader/ZipDownloaderTest.php | 1 - .../Test/Plugin/PluginInstallerTest.php | 6 ++--- .../Test/Repository/Vcs/GitLabDriverTest.php | 2 +- .../Test/Util/RemoteFilesystemTest.php | 2 +- tests/Composer/Test/Util/SilencerTest.php | 5 ++-- 26 files changed, 75 insertions(+), 75 deletions(-) diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index 87fdc4ffb..94fccc2e5 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -332,11 +332,11 @@ EOT 'disable-tls' => array($booleanValidator, $booleanNormalizer), 'cafile' => array( function ($val) { return file_exists($val) && is_readable($val); }, - function ($val) { return $val === 'null' ? null : $val; } + function ($val) { return $val === 'null' ? null : $val; }, ), 'capath' => array( function ($val) { return is_dir($val) && is_readable($val); }, - function ($val) { return $val === 'null' ? null : $val; } + function ($val) { return $val === 'null' ? null : $val; }, ), 'github-expose-hostname' => array($booleanValidator, $booleanNormalizer), ); diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index dbb7f6802..4ddc2cfa7 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -36,7 +36,6 @@ use Symfony\Component\Finder\Finder; use Composer\Json\JsonFile; use Composer\Config\JsonConfigSource; use Composer\Util\Filesystem; -use Composer\Util\RemoteFilesystem; use Composer\Package\Version\VersionParser; /** diff --git a/src/Composer/Command/DependsCommand.php b/src/Composer/Command/DependsCommand.php index a58e7c5be..abdce5a86 100644 --- a/src/Composer/Command/DependsCommand.php +++ b/src/Composer/Command/DependsCommand.php @@ -132,7 +132,7 @@ EOT } else { $matchText = ''; if ($input->getOption('match-constraint') !== '*') { - $matchText = ' in versions '.($matchInvert ? 'not ':'').'matching ' . $input->getOption('match-constraint'); + $matchText = ' in versions '.($matchInvert ? 'not ' : '').'matching ' . $input->getOption('match-constraint'); } $io->writeError('There is no installed package depending on "'.$needle.'"'.$matchText.'.'); } diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 7d868da06..fff159158 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -18,7 +18,6 @@ use Composer\Config; use Composer\Util\Filesystem; use Composer\Util\Keys; use Composer\IO\IOInterface; -use Composer\Util\RemoteFilesystem; use Composer\Downloader\FilesystemException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -232,6 +231,7 @@ TAGSPUBKEY if (!preg_match('{^-----BEGIN PUBLIC KEY-----$}', trim($value))) { throw new \UnexpectedValueException('Invalid input'); } + return trim($value)."\n"; }; diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 7704e09fb..7f0c77537 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -458,7 +458,7 @@ EOT /** * Init styles for tree * - * @param OutputInterface $output + * @param OutputInterface $output */ protected function initStyles(OutputInterface $output) { @@ -479,10 +479,10 @@ EOT /** * Display the tree * - * @param PackageInterface|string $package - * @param RepositoryInterface $installedRepo - * @param RepositoryInterface $distantRepos - * @param OutputInterface $output + * @param PackageInterface|string $package + * @param RepositoryInterface $installedRepo + * @param RepositoryInterface $distantRepos + * @param OutputInterface $output */ protected function displayPackageTree(PackageInterface $package, RepositoryInterface $installedRepo, RepositoryInterface $distantRepos, OutputInterface $output) { @@ -524,14 +524,14 @@ EOT /** * Display a package tree * - * @param string $name - * @param PackageInterface|string $package - * @param RepositoryInterface $installedRepo - * @param RepositoryInterface $distantRepos - * @param array $packagesInTree - * @param OutputInterface $output - * @param string $previousTreeBar - * @param integer $level + * @param string $name + * @param PackageInterface|string $package + * @param RepositoryInterface $installedRepo + * @param RepositoryInterface $distantRepos + * @param array $packagesInTree + * @param OutputInterface $output + * @param string $previousTreeBar + * @param int $level */ protected function displayTree($name, $package, RepositoryInterface $installedRepo, RepositoryInterface $distantRepos, array $packagesInTree, OutputInterface $output, $previousTreeBar = '├', $level = 1) { diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 10ad2762b..632741586 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -16,7 +16,6 @@ use Composer\Util\Silencer; use Symfony\Component\Console\Application as BaseApplication; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Formatter\OutputFormatter; diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index fcbb6a77b..a457c5c63 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -82,7 +82,7 @@ class SolverProblemsException extends \RuntimeException private function hasExtensionProblems(array $reasonSets) { foreach ($reasonSets as $reasonSet) { - foreach($reasonSet as $reason) { + foreach ($reasonSet as $reason) { if (isset($reason["rule"]) && 0 === strpos($reason["rule"]->getRequiredPackage(), 'ext-')) { return true; } diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index eb6072709..e11677b56 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -41,8 +41,8 @@ use Seld\JsonLint\JsonParser; class Factory { /** - * @return string * @throws \RuntimeException + * @return string */ protected static function getHomeDir() { @@ -115,7 +115,7 @@ class Factory } /** - * @param string $home + * @param string $home * @return string */ protected static function getDataDir($home) @@ -140,7 +140,7 @@ class Factory } /** - * @param IOInterface|null $io + * @param IOInterface|null $io * @return Config */ public static function createConfig(IOInterface $io = null, $cwd = null) @@ -569,9 +569,9 @@ class Factory } /** - * @param IOInterface $io IO instance - * @param Config $config Config instance - * @param array $options Array of options passed directly to RemoteFilesystem constructor + * @param IOInterface $io IO instance + * @param Config $config Config instance + * @param array $options Array of options passed directly to RemoteFilesystem constructor * @return RemoteFilesystem */ public static function createRemoteFilesystem(IOInterface $io, Config $config = null, $options = array()) @@ -616,7 +616,7 @@ class Factory } /** - * @return boolean + * @return bool */ private static function useXdg() { @@ -630,8 +630,8 @@ class Factory } /** - * @return string * @throws \RuntimeException + * @return string */ private static function getUserDir() { diff --git a/src/Composer/Package/AliasPackage.php b/src/Composer/Package/AliasPackage.php index f6849a234..e161a4482 100644 --- a/src/Composer/Package/AliasPackage.php +++ b/src/Composer/Package/AliasPackage.php @@ -165,7 +165,7 @@ class AliasPackage extends BasePackage implements CompletePackageInterface } /** - * @param Link[] $links + * @param Link[] $links * @param string $linkType * * @return Link[] diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index 49a783579..bfb0d9427 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -127,6 +127,7 @@ class PluginManager $this->io->writeError('The "' . $package->getName() . '" plugin requires composer-plugin-api 1.0.0, this *WILL* break in the future and it should be fixed ASAP (require ^1.0 for example).'); } elseif (!$requiresComposer->matches($currentPluginApiConstraint)) { $this->io->writeError('The "' . $package->getName() . '" plugin was skipped because it requires a Plugin API version ("' . $requiresComposer->getPrettyString() . '") that does not match your Composer installation ("' . $currentPluginApiVersion . '"). You may need to run composer update with the "--no-plugins" option.'); + return; } } @@ -304,10 +305,10 @@ class PluginManager } /** - * @param PluginInterface $plugin - * @param string $capability - * @return null|string The fully qualified class of the implementation or null if Plugin is not of Capable type or does not provide it + * @param PluginInterface $plugin + * @param string $capability * @throws \RuntimeException On empty or non-string implementation class name value + * @return null|string The fully qualified class of the implementation or null if Plugin is not of Capable type or does not provide it */ protected function getCapabilityImplementationClassName(PluginInterface $plugin, $capability) { @@ -330,11 +331,11 @@ class PluginManager } /** - * @param PluginInterface $plugin - * @param string $capabilityClassName The fully qualified name of the API interface which the plugin may provide - * an implementation of. - * @param array $ctorArgs Arguments passed to Capability's constructor. - * Keeping it an array will allow future values to be passed w\o changing the signature. + * @param PluginInterface $plugin + * @param string $capabilityClassName The fully qualified name of the API interface which the plugin may provide + * an implementation of. + * @param array $ctorArgs Arguments passed to Capability's constructor. + * Keeping it an array will allow future values to be passed w\o changing the signature. * @return null|Capability */ public function getPluginCapability(PluginInterface $plugin, $capabilityClassName, array $ctorArgs = array()) diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 6806d1c8a..f3cf20e29 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -747,6 +747,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->io->writeError(''.$this->url.' could not be fully loaded, package information was loaded from the local cache and may be out of date'); } $this->degradedMode = true; + return true; } } diff --git a/src/Composer/Repository/PathRepository.php b/src/Composer/Repository/PathRepository.php index 4529af0f0..c3266543b 100644 --- a/src/Composer/Repository/PathRepository.php +++ b/src/Composer/Repository/PathRepository.php @@ -155,7 +155,7 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn private function getUrlMatches() { // Ensure environment-specific path separators are normalized to URL separators - return array_map(function($val) { + return array_map(function ($val) { return str_replace(DIRECTORY_SEPARATOR, '/', $val); }, glob($this->url, GLOB_MARK | GLOB_ONLYDIR)); } diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index 084bc2af7..833c82c20 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -203,6 +203,7 @@ class PlatformRepository extends ArrayRepository if (isset($this->overrides[strtolower($package->getName())])) { $overrider = $this->findPackage($package->getName(), '*'); $overrider->setDescription($overrider->getDescription().' (actual: '.$package->getPrettyVersion().')'); + return; } parent::addPackage($package); diff --git a/src/Composer/Repository/RepositoryManager.php b/src/Composer/Repository/RepositoryManager.php index 7f91ac6ff..42c14106f 100644 --- a/src/Composer/Repository/RepositoryManager.php +++ b/src/Composer/Repository/RepositoryManager.php @@ -105,7 +105,6 @@ class RepositoryManager $class = $this->repositoryClasses[$type]; - $reflMethod = new \ReflectionMethod($class, '__construct'); $params = $reflMethod->getParameters(); if (isset($params[4]) && $params[4]->getClass() && $params[4]->getClass()->getName() === 'Composer\Util\RemoteFilesystem') { diff --git a/src/Composer/Util/ConfigValidator.php b/src/Composer/Util/ConfigValidator.php index cb5706c88..f36ff8e6f 100644 --- a/src/Composer/Util/ConfigValidator.php +++ b/src/Composer/Util/ConfigValidator.php @@ -19,7 +19,6 @@ use Composer\Json\JsonValidationException; use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Spdx\SpdxLicenses; -use Composer\Factory; /** * Validates a composer configuration. diff --git a/src/Composer/Util/Keys.php b/src/Composer/Util/Keys.php index 19628f5d3..4afa204cd 100644 --- a/src/Composer/Util/Keys.php +++ b/src/Composer/Util/Keys.php @@ -12,8 +12,6 @@ namespace Composer\Util; -use Composer\Config; - /** * @author Jordi Boggiano */ @@ -23,7 +21,7 @@ class Keys { $hash = strtoupper(hash('sha256', preg_replace('{\s}', '', file_get_contents($path)))); - return implode(' ', [ + return implode(' ', array( substr($hash, 0, 8), substr($hash, 8, 8), substr($hash, 16, 8), @@ -33,6 +31,6 @@ class Keys substr($hash, 40, 8), substr($hash, 48, 8), substr($hash, 56, 8), - ]); + )); } } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 1a53c994f..bb351dcdd 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -134,8 +134,8 @@ class RemoteFilesystem } /** - * @param array $headers array of returned headers like from getLastHeaders() - * @param string $name header name (case insensitive) + * @param array $headers array of returned headers like from getLastHeaders() + * @param string $name header name (case insensitive) * @return string|null */ public function findHeaderValue(array $headers, $name) @@ -155,7 +155,7 @@ class RemoteFilesystem } /** - * @param array $headers array of returned headers like from getLastHeaders() + * @param array $headers array of returned headers like from getLastHeaders() * @return int|null */ public function findStatusCode(array $headers) @@ -752,7 +752,7 @@ class RemoteFilesystem '!DES', '!3DES', '!MD5', - '!PSK' + '!PSK', )); /** @@ -768,7 +768,7 @@ class RemoteFilesystem 'verify_depth' => 7, 'SNI_enabled' => true, 'capture_peer_cert' => true, - ) + ), ); if (isset($options['ssl'])) { @@ -969,7 +969,7 @@ class RemoteFilesystem 'ssl' => array( 'capture_peer_cert' => true, 'verify_peer' => false, // Yes this is fucking insane! But PHP is lame. - )) + ), ), )); // Ideally this would just use stream_socket_client() to avoid sending a diff --git a/src/Composer/Util/Silencer.php b/src/Composer/Util/Silencer.php index bc09d5efd..03cfff430 100644 --- a/src/Composer/Util/Silencer.php +++ b/src/Composer/Util/Silencer.php @@ -26,16 +26,18 @@ class Silencer /** * Suppresses given mask or errors. * - * @param int|null $mask Error levels to suppress, default value NULL indicates all warnings and below. - * @return int The old error reporting level. + * @param int|null $mask Error levels to suppress, default value NULL indicates all warnings and below. + * @return int The old error reporting level. */ public static function suppress($mask = null) { if (!isset($mask)) { $mask = E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_DEPRECATED | E_USER_DEPRECATED | E_STRICT; } - array_push(self::$stack, $old = error_reporting()); + $old = error_reporting(); + array_push(self::$stack, $old); error_reporting($old & ~$mask); + return $old; } @@ -44,8 +46,9 @@ class Silencer */ public static function restore() { - if (!empty(self::$stack)) + if (!empty(self::$stack)) { error_reporting(array_pop(self::$stack)); + } } /** @@ -53,9 +56,9 @@ class Silencer * * Future improvement: when PHP requirements are raised add Callable type hint (5.4) and variadic parameters (5.6) * - * @param callable $callable Function to execute. - * @return mixed Return value of the callback. + * @param callable $callable Function to execute. * @throws \Exception Any exceptions from the callback are rethrown. + * @return mixed Return value of the callback. */ public static function call($callable /*, ...$parameters */) { @@ -63,11 +66,12 @@ class Silencer self::suppress(); $result = call_user_func_array($callable, array_slice(func_get_args(), 1)); self::restore(); + return $result; - } catch(\Exception $e) { + } catch (\Exception $e) { // Use a finally block for this when requirements are raised to PHP 5.5 self::restore(); throw $e; } } -} \ No newline at end of file +} diff --git a/src/Composer/Util/TlsHelper.php b/src/Composer/Util/TlsHelper.php index 4aea24df6..6ea5cf591 100644 --- a/src/Composer/Util/TlsHelper.php +++ b/src/Composer/Util/TlsHelper.php @@ -24,9 +24,9 @@ final class TlsHelper /** * Match hostname against a certificate. * - * @param mixed $certificate X.509 certificate - * @param string $hostname Hostname in the URL - * @param string $cn Set to the common name of the certificate iff match found + * @param mixed $certificate X.509 certificate + * @param string $hostname Hostname in the URL + * @param string $cn Set to the common name of the certificate iff match found * * @return bool */ @@ -46,6 +46,7 @@ final class TlsHelper if ($matcher && $matcher($hostname)) { $cn = $names['cn']; + return true; } } @@ -53,7 +54,6 @@ final class TlsHelper return false; } - /** * Extract DNS names out of an X.509 certificate. * diff --git a/tests/Composer/Test/Config/JsonConfigSourceTest.php b/tests/Composer/Test/Config/JsonConfigSourceTest.php index 5877f69fb..e558932c2 100644 --- a/tests/Composer/Test/Config/JsonConfigSourceTest.php +++ b/tests/Composer/Test/Config/JsonConfigSourceTest.php @@ -62,9 +62,9 @@ class JsonConfigSourceTest extends TestCase 'url' => 'https://example.tld', 'options' => array( 'ssl' => array( - 'local_cert' => '/home/composer/.ssl/composer.pem' - ) - ) + 'local_cert' => '/home/composer/.ssl/composer.pem', + ), + ), )); $this->assertFileEquals($this->fixturePath('config/config-with-exampletld-repository-and-options.json'), $config); diff --git a/tests/Composer/Test/DefaultConfigTest.php b/tests/Composer/Test/DefaultConfigTest.php index 4cca5025a..74a74aecd 100644 --- a/tests/Composer/Test/DefaultConfigTest.php +++ b/tests/Composer/Test/DefaultConfigTest.php @@ -24,5 +24,4 @@ class DefaultConfigTest extends \PHPUnit_Framework_TestCase $config = new Config; $this->assertFalse($config->get('disable-tls')); } - -} \ No newline at end of file +} diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index a0507e730..f70d9e44c 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -18,7 +18,6 @@ use Composer\Util\Filesystem; class ZipDownloaderTest extends TestCase { - /** * @var string */ diff --git a/tests/Composer/Test/Plugin/PluginInstallerTest.php b/tests/Composer/Test/Plugin/PluginInstallerTest.php index 8d3a5e4dc..50c7178da 100644 --- a/tests/Composer/Test/Plugin/PluginInstallerTest.php +++ b/tests/Composer/Test/Plugin/PluginInstallerTest.php @@ -315,7 +315,7 @@ class PluginInstallerTest extends TestCase $plugin->expects($this->once()) ->method('getCapabilities') - ->will($this->returnCallback(function() use ($capabilityImplementation, $capabilityApi) { + ->will($this->returnCallback(function () use ($capabilityImplementation, $capabilityApi) { return array($capabilityApi => $capabilityImplementation); })); @@ -361,7 +361,7 @@ class PluginInstallerTest extends TestCase $plugin->expects($this->once()) ->method('getCapabilities') - ->will($this->returnCallback(function() use ($invalidImplementationClassNames, $capabilityApi) { + ->will($this->returnCallback(function () use ($invalidImplementationClassNames, $capabilityApi) { return array($capabilityApi => $invalidImplementationClassNames); })); @@ -377,7 +377,7 @@ class PluginInstallerTest extends TestCase $plugin->expects($this->once()) ->method('getCapabilities') - ->will($this->returnCallback(function() { + ->will($this->returnCallback(function () { return array(); })); diff --git a/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php index e1ac82021..70bb94843 100644 --- a/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php @@ -35,7 +35,7 @@ class GitLabDriverTest extends TestCase $this->config->merge(array( 'config' => array( 'home' => $this->home, - 'gitlab-domains' => array('mycompany.com/gitlab', 'gitlab.com') + 'gitlab-domains' => array('mycompany.com/gitlab', 'gitlab.com'), ), )); diff --git a/tests/Composer/Test/Util/RemoteFilesystemTest.php b/tests/Composer/Test/Util/RemoteFilesystemTest.php index 6647e6d5c..73861e396 100644 --- a/tests/Composer/Test/Util/RemoteFilesystemTest.php +++ b/tests/Composer/Test/Util/RemoteFilesystemTest.php @@ -172,7 +172,7 @@ class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase { $io = $this->getMock('Composer\IO\IOInterface'); - $res = $this->callGetOptionsForUrl($io, array('example.org', array('ssl'=>array('cafile'=>'/some/path/file.crt'))), array(), 'http://www.example.org'); + $res = $this->callGetOptionsForUrl($io, array('example.org', array('ssl' => array('cafile' => '/some/path/file.crt'))), array(), 'http://www.example.org'); $this->assertTrue(isset($res['ssl']['ciphers'])); $this->assertRegExp("|!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK|", $res['ssl']['ciphers']); diff --git a/tests/Composer/Test/Util/SilencerTest.php b/tests/Composer/Test/Util/SilencerTest.php index 3bf913480..5201522f8 100644 --- a/tests/Composer/Test/Util/SilencerTest.php +++ b/tests/Composer/Test/Util/SilencerTest.php @@ -33,8 +33,9 @@ class SilencerTest extends \PHPUnit_Framework_TestCase Silencer::restore(); // Check all parameters and return values are passed correctly in a silenced call. - $result = Silencer::call(function($a, $b, $c) { + $result = Silencer::call(function ($a, $b, $c) { @trigger_error('Test', E_USER_WARNING); + return $a * $b * $c; }, 2, 3, 4); $this->assertEquals(24, $result); @@ -50,7 +51,7 @@ class SilencerTest extends \PHPUnit_Framework_TestCase { $verification = microtime(); $this->setExpectedException('\RuntimeException', $verification); - Silencer::call(function() use ($verification) { + Silencer::call(function () use ($verification) { throw new \RuntimeException($verification); }); } From f1fd7d1dd60577c6c8f4bb0c63a3548a90fd7c4e Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Tue, 26 Jan 2016 14:03:08 +0100 Subject: [PATCH 68/98] make clashes less likely and use silencer --- tests/Composer/TestCase.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/Composer/TestCase.php b/tests/Composer/TestCase.php index 299ac9903..7186ae556 100644 --- a/tests/Composer/TestCase.php +++ b/tests/Composer/TestCase.php @@ -16,6 +16,7 @@ use Composer\Semver\VersionParser; use Composer\Package\AliasPackage; use Composer\Semver\Constraint\Constraint; use Composer\Util\Filesystem; +use Composer\Util\Silencer; abstract class TestCase extends \PHPUnit_Framework_TestCase { @@ -62,8 +63,9 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase $root = sys_get_temp_dir(); do { - $unique = $root . DIRECTORY_SEPARATOR . uniqid('composer-test-'); - if (!file_exists($unique) && false !== @mkdir($unique, 0777)) { + $unique = $root . DIRECTORY_SEPARATOR . uniqid('composer-test-' . rand(1000, 9000)); + + if (!file_exists($unique) && Silencer::call('mkdir', $unique, 0777)) { return realpath($unique); } } while (--$attempts); From f829a160fb2197cfde52a95ab89cf8515b101d90 Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Tue, 26 Jan 2016 14:32:04 +0100 Subject: [PATCH 69/98] use a proper tmp directory this test failed on OSX before, trying to create a directory at the root of the filesystem --- src/Composer/Command/CreateProjectCommand.php | 2 +- .../Test/Repository/RepositoryManagerTest.php | 33 +++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 4ddc2cfa7..ace946f3a 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -294,7 +294,7 @@ EOT // handler Ctrl+C for unix-like systems if (function_exists('pcntl_signal')) { - declare (ticks = 100); + declare(ticks=100); pcntl_signal(SIGINT, function () use ($directory) { $fs = new Filesystem(); $fs->removeDirectory($directory); diff --git a/tests/Composer/Test/Repository/RepositoryManagerTest.php b/tests/Composer/Test/Repository/RepositoryManagerTest.php index 4293dff66..0a419be6c 100644 --- a/tests/Composer/Test/Repository/RepositoryManagerTest.php +++ b/tests/Composer/Test/Repository/RepositoryManagerTest.php @@ -13,22 +13,49 @@ namespace Composer\Repository; use Composer\TestCase; +use Composer\Util\Filesystem; class RepositoryManagerTest extends TestCase { + protected $tmpdir; + + public function setUp() + { + $this->tmpdir = $this->getUniqueTmpDirectory(); + } + + public function tearDown() + { + if (is_dir($this->tmpdir)) { + $fs = new Filesystem(); + $fs->removeDirectory($this->tmpdir); + } + } + /** * @dataProvider creationCases */ - public function testRepoCreation($type, $config, $exception = null) + public function testRepoCreation($type, $options, $exception = null) { if ($exception) { $this->setExpectedException($exception); } + $rm = new RepositoryManager( $this->getMock('Composer\IO\IOInterface'), - $this->getMock('Composer\Config'), + $config = $this->getMock('Composer\Config', array('get')), $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock() ); + + $tmpdir = $this->tmpdir; + $config + ->expects($this->any()) + ->method('get') + ->will($this->returnCallback(function ($arg) use ($tmpdir) { + return 'cache-repo-dir' === $arg ? $tmpdir : null; + })) + ; + $rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository'); $rm->setRepositoryClass('vcs', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository'); @@ -40,7 +67,7 @@ class RepositoryManagerTest extends TestCase $rm->setRepositoryClass('artifact', 'Composer\Repository\ArtifactRepository'); $rm->createRepository('composer', array('url' => 'http://example.org')); - $rm->createRepository($type, $config); + $rm->createRepository($type, $options); } public function creationCases() From 594cf658da8f9a67a8ce4955af943c9fcc738e79 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 26 Jan 2016 13:59:34 +0000 Subject: [PATCH 70/98] Update php-cs-fixer config --- .php_cs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.php_cs b/.php_cs index a2bd217ed..b9f3876ef 100644 --- a/.php_cs +++ b/.php_cs @@ -10,7 +10,7 @@ For the full copyright and license information, please view the LICENSE file that was distributed with this source code. EOF; -$finder = Symfony\CS\Finder\DefaultFinder::create() +$finder = Symfony\CS\Finder::create() ->files() ->name('*.php') ->exclude('Fixtures') @@ -18,23 +18,27 @@ $finder = Symfony\CS\Finder\DefaultFinder::create() ->in(__DIR__.'/tests') ; -return Symfony\CS\Config\Config::create() +return Symfony\CS\Config::create() ->setUsingCache(true) ->setRiskyAllowed(true) ->setRules(array( '@PSR2' => true, - 'duplicate_semicolon' => true, - 'extra_empty_lines' => true, + 'binary_operator_spaces' => true, + 'blank_line_before_return' => true, 'header_comment' => array('header' => $header), 'include' => true, 'long_array_syntax' => true, 'method_separation' => true, - 'multiline_array_trailing_comma' => true, - 'namespace_no_leading_whitespace' => true, 'no_blank_lines_after_class_opening' => true, - 'no_empty_lines_after_phpdocs' => true, - 'object_operator' => true, - 'operators_spaces' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_blank_lines_between_uses' => true, + 'no_duplicate_semicolons' => true, + 'no_extra_consecutive_blank_lines' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'no_unused_imports' => true, + 'object_operator_without_whitespace' => true, 'phpdoc_align' => true, 'phpdoc_indent' => true, 'phpdoc_no_access' => true, @@ -44,15 +48,11 @@ return Symfony\CS\Config\Config::create() 'phpdoc_trim' => true, 'phpdoc_type_to_var' => true, 'psr0' => true, - 'return' => true, - 'remove_leading_slash_use' => true, - 'remove_lines_between_uses' => true, - 'single_array_no_trailing_comma' => true, 'single_blank_line_before_namespace' => true, 'spaces_cast' => true, - 'standardize_not_equal' => true, - 'ternary_spaces' => true, - 'unused_use' => true, + 'standardize_not_equals' => true, + 'ternary_operator_spaces' => true, + 'trailing_comma_in_multiline_array' => true, 'whitespacy_lines' => true, )) ->finder($finder) From 4e0063529882dc30218d4bd72f0f596600c27ae7 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 26 Jan 2016 15:04:00 +0000 Subject: [PATCH 71/98] Fix error handling support, fixes #4833 --- src/Composer/Util/ErrorHandler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Composer/Util/ErrorHandler.php b/src/Composer/Util/ErrorHandler.php index 399491f8c..67454271d 100644 --- a/src/Composer/Util/ErrorHandler.php +++ b/src/Composer/Util/ErrorHandler.php @@ -36,8 +36,8 @@ class ErrorHandler */ public static function handle($level, $message, $file, $line) { - // respect error_reporting being disabled - if (!error_reporting()) { + // error code is not included in error_reporting + if (!(error_reporting() & $level)) { return; } From 618e7f98b2c7f80dbe13cf1316b533a37a854829 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 26 Jan 2016 15:05:57 +0000 Subject: [PATCH 72/98] Force base error reporting level to include everything --- src/Composer/Util/ErrorHandler.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Composer/Util/ErrorHandler.php b/src/Composer/Util/ErrorHandler.php index 67454271d..ddb4b570b 100644 --- a/src/Composer/Util/ErrorHandler.php +++ b/src/Composer/Util/ErrorHandler.php @@ -73,6 +73,7 @@ class ErrorHandler public static function register(IOInterface $io = null) { set_error_handler(array(__CLASS__, 'handle')); + error_reporting(E_ALL | E_STRICT); self::$io = $io; } } From 13b50799d199dd104ffe858915143808409f1554 Mon Sep 17 00:00:00 2001 From: appchecker Date: Tue, 26 Jan 2016 18:19:58 +0300 Subject: [PATCH 73/98] fix: missing parentheses --- src/Composer/Config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Config.php b/src/Composer/Config.php index e8fdf2561..2b6d14da7 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -191,7 +191,7 @@ class Config return $val; } - return ($flags & self::RELATIVE_PATHS == self::RELATIVE_PATHS) ? $val : $this->realpath($val); + return (($flags & self::RELATIVE_PATHS) == self::RELATIVE_PATHS) ? $val : $this->realpath($val); case 'cache-ttl': return (int) $this->config[$key]; From ae14e0f086726dde35c1bc4cee0578babd81dab5 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 26 Jan 2016 16:53:09 +0000 Subject: [PATCH 74/98] Add ssh2 protocol default ports, fixes #4835 --- src/Composer/Util/RemoteFilesystem.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index bb351dcdd..2d94e5909 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -1002,6 +1002,8 @@ class RemoteFilesystem 'ftp' => 21, 'http' => 80, 'https' => 443, + 'ssh2.sftp' => 22, + 'ssh2.scp' => 22, ); $scheme = parse_url($url, PHP_URL_SCHEME); From 593b88e41487f481d4368a4d331146914c1c2981 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 26 Jan 2016 19:09:44 +0000 Subject: [PATCH 75/98] Let users configure *any auth* via COMPOSER_AUTH and add it to the docs, refs #4546 --- doc/03-cli.md | 7 +++++++ src/Composer/IO/BaseIO.php | 32 +++++++++++++++++++++----------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/doc/03-cli.md b/doc/03-cli.md index cb47f7cc4..94a7a963c 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -717,6 +717,13 @@ commands) to finish executing. The default value is 300 seconds (5 minutes). By setting this environmental value, you can set a path to a certificate bundle file to be used during SSL/TLS peer verification. +### COMPOSER_AUTH + +The `COMPOSER_AUTH` var allows you to set up authentication as an environment variable. +The contents of the variable should be a JSON formatted object containing http-basic, +github-oauth, ... objects as needed, and following the +[spec from the config](06-config.md#gitlab-oauth). + ### COMPOSER_DISCARD_CHANGES This env var controls the [`discard-changes`](06-config.md#discard-changes) config option. diff --git a/src/Composer/IO/BaseIO.php b/src/Composer/IO/BaseIO.php index 43248c92c..3a5a1d701 100644 --- a/src/Composer/IO/BaseIO.php +++ b/src/Composer/IO/BaseIO.php @@ -60,22 +60,32 @@ abstract class BaseIO implements IOInterface */ public function loadConfiguration(Config $config) { + $githubOauth = $config->get('github-oauth'); + $gitlabOauth = $config->get('gitlab-oauth'); + $httpBasic = $config->get('http-basic'); + // Use COMPOSER_AUTH environment variable if set - if ($envvar_data = getenv('COMPOSER_AUTH')) { - $auth_data = json_decode($envvar_data); + if ($composerAuthEnv = getenv('COMPOSER_AUTH')) { + $authData = json_decode($composerAuthEnv, true); - if (is_null($auth_data)) { + if (is_null($authData)) { throw new \UnexpectedValueException('COMPOSER_AUTH environment variable is malformed'); } - foreach ($auth_data as $domain => $credentials) { - $this->setAuthentication($domain, $credentials->username, $credentials->password); + if (isset($authData['github-oauth'])) { + $githubOauth = array_merge($githubOauth, $authData['github-oauth']); + } + if (isset($authData['gitlab-oauth'])) { + $gitlabOauth = array_merge($gitlabOauth, $authData['gitlab-oauth']); + } + if (isset($authData['http-basic'])) { + $httpBasic = array_merge($httpBasic, $authData['http-basic']); } } // reload oauth token from config if available - if ($tokens = $config->get('github-oauth')) { - foreach ($tokens as $domain => $token) { + if ($githubOauth) { + foreach ($githubOauth as $domain => $token) { if (!preg_match('{^[a-z0-9]+$}', $token)) { throw new \UnexpectedValueException('Your github oauth token for '.$domain.' contains invalid characters: "'.$token.'"'); } @@ -83,15 +93,15 @@ abstract class BaseIO implements IOInterface } } - if ($tokens = $config->get('gitlab-oauth')) { - foreach ($tokens as $domain => $token) { + if ($gitlabOauth) { + foreach ($gitlabOauth as $domain => $token) { $this->setAuthentication($domain, $token, 'oauth2'); } } // reload http basic credentials from config if available - if ($creds = $config->get('http-basic')) { - foreach ($creds as $domain => $cred) { + if ($httpBasic) { + foreach ($httpBasic as $domain => $cred) { $this->setAuthentication($domain, $cred['username'], $cred['password']); } } From a48159b2838bcbd429ee57e31b848e5db26239a1 Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Tue, 26 Jan 2016 23:39:39 +0100 Subject: [PATCH 76/98] Bail out if root package attempts to include itself. --- src/Composer/Package/Loader/RootPackageLoader.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Composer/Package/Loader/RootPackageLoader.php b/src/Composer/Package/Loader/RootPackageLoader.php index 0fc7c49af..6f8d45f31 100644 --- a/src/Composer/Package/Loader/RootPackageLoader.php +++ b/src/Composer/Package/Loader/RootPackageLoader.php @@ -113,6 +113,9 @@ class RootPackageLoader extends ArrayLoader } } + if (isset($links[$config['name']])) + throw new \InvalidArgumentException(sprintf('Root package \'%s\' cannot require itself in its composer.json', $config['name'])); + $realPackage->setAliases($aliases); $realPackage->setStabilityFlags($stabilityFlags); $realPackage->setReferences($references); From b1de2c52a3b969b03d258ba15fa68c8b0e7c0e2c Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Wed, 27 Jan 2016 00:48:17 +0100 Subject: [PATCH 77/98] Add --no-plugins option to remove/require --- src/Composer/Command/RemoveCommand.php | 3 ++- src/Composer/Command/RequireCommand.php | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index 869720718..e9bd610b4 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -37,6 +37,7 @@ class RemoveCommand extends Command ->setDefinition(array( new InputArgument('packages', InputArgument::IS_ARRAY, 'Packages that should be removed.'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Removes a package from the require-dev section.'), + new InputOption('no-plugins', null, InputOption::VALUE_NONE, 'Disables all plugins.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'), new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'), @@ -92,7 +93,7 @@ EOT } // Update packages - $composer = $this->getComposer(); + $composer = $this->getComposer(true, $input->getOption('no-plugins')); $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output); diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index bbdf15681..cd62bbc5a 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -42,6 +42,7 @@ class RequireCommand extends InitCommand new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist even for dev versions.'), + new InputOption('no-plugins', null, InputOption::VALUE_NONE, 'Disables all plugins.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies.'), new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'), @@ -93,7 +94,7 @@ EOT $composerDefinition = $json->read(); $composerBackup = file_get_contents($json->getPath()); - $composer = $this->getComposer(); + $composer = $this->getComposer(true, $input->getOption('no-plugins')); $repos = $composer->getRepositoryManager()->getRepositories(); $platformOverrides = $composer->getConfig()->get('platform') ?: array(); @@ -143,7 +144,7 @@ EOT // Update packages $this->resetComposer(); - $composer = $this->getComposer(); + $composer = $this->getComposer(true, $input->getOption('no-plugins')); $composer->getDownloadManager()->setOutputProgress(!$input->getOption('no-progress')); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output); From d58a788485204ffd0fd0d5b0c53bbfdf7b42acf2 Mon Sep 17 00:00:00 2001 From: Stephen Beemsterboer Date: Mon, 18 Jan 2016 21:34:55 -0500 Subject: [PATCH 78/98] Clarify "path" repository type version requirement --- doc/05-repositories.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/05-repositories.md b/doc/05-repositories.md index 7541f2a22..7bd3d1fe8 100644 --- a/doc/05-repositories.md +++ b/doc/05-repositories.md @@ -639,6 +639,11 @@ file, you can use the following configuration: } ``` +If the package is a local VCS repository, the version may be inferred by +the branch or tag that is currently checked out. Otherwise, the version should +be explicitly defined in the package's `composer.json` file. If the version +cannot be resolved by these means, it is assumed to be `dev-master`. + The local package will be symlinked if possible, in which case the output in the console will read `Symlinked from ../../packages/my-package`. If symlinking is _not_ possible the package will be copied. In that case, the console will From 7b6ccde97afe1e85029dab144cc57224d2986d02 Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Wed, 27 Jan 2016 09:09:29 +0100 Subject: [PATCH 79/98] Clarified error message and added braces. --- src/Composer/Package/Loader/RootPackageLoader.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Composer/Package/Loader/RootPackageLoader.php b/src/Composer/Package/Loader/RootPackageLoader.php index 6f8d45f31..565ff39c4 100644 --- a/src/Composer/Package/Loader/RootPackageLoader.php +++ b/src/Composer/Package/Loader/RootPackageLoader.php @@ -113,8 +113,10 @@ class RootPackageLoader extends ArrayLoader } } - if (isset($links[$config['name']])) - throw new \InvalidArgumentException(sprintf('Root package \'%s\' cannot require itself in its composer.json', $config['name'])); + if (isset($links[$config['name']])) { + throw new \InvalidArgumentException(sprintf('Root package \'%s\' cannot require itself in its composer.json' . PHP_EOL . + 'Did you accidentally name your root package after an external package?', $config['name'])); + } $realPackage->setAliases($aliases); $realPackage->setStabilityFlags($stabilityFlags); From e5fe3d8a3bf3e5048a5ec3ad4c6a7ea15792cf09 Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Wed, 27 Jan 2016 10:04:45 +0100 Subject: [PATCH 80/98] Expanded InstallerTest to support expecting Exceptions by supplying "EXCEPTION" as "--EXPECT--" --- tests/Composer/Test/InstallerTest.php | 183 ++++++++++++++------------ 1 file changed, 98 insertions(+), 85 deletions(-) diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 5339b8ff3..f72c6d9d5 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -158,96 +158,109 @@ class InstallerTest extends TestCase ->method('writeError') ->will($this->returnCallback($callback)); - $composer = FactoryMock::create($io, $composerConfig); - - $jsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); - $jsonMock->expects($this->any()) - ->method('read') - ->will($this->returnValue($installed)); - $jsonMock->expects($this->any()) - ->method('exists') - ->will($this->returnValue(true)); - - $repositoryManager = $composer->getRepositoryManager(); - $repositoryManager->setLocalRepository(new InstalledFilesystemRepositoryMock($jsonMock)); - - $lockJsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); - $lockJsonMock->expects($this->any()) - ->method('read') - ->will($this->returnValue($lock)); - $lockJsonMock->expects($this->any()) - ->method('exists') - ->will($this->returnValue(true)); - - if ($expectLock) { - $actualLock = array(); - $lockJsonMock->expects($this->atLeastOnce()) - ->method('write') - ->will($this->returnCallback(function ($hash, $options) use (&$actualLock) { - // need to do assertion outside of mock for nice phpunit output - // so store value temporarily in reference for later assetion - $actualLock = $hash; - })); - } - - $contents = json_encode($composerConfig); - $locker = new Locker($io, $lockJsonMock, $repositoryManager, $composer->getInstallationManager(), $contents); - $composer->setLocker($locker); + // Prepare for exceptions + try { + $composer = FactoryMock::create($io, $composerConfig); + + $jsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); + $jsonMock->expects($this->any()) + ->method('read') + ->will($this->returnValue($installed)); + $jsonMock->expects($this->any()) + ->method('exists') + ->will($this->returnValue(true)); + + $repositoryManager = $composer->getRepositoryManager(); + $repositoryManager->setLocalRepository(new InstalledFilesystemRepositoryMock($jsonMock)); + + $lockJsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); + $lockJsonMock->expects($this->any()) + ->method('read') + ->will($this->returnValue($lock)); + $lockJsonMock->expects($this->any()) + ->method('exists') + ->will($this->returnValue(true)); + + if ($expectLock) { + $actualLock = array(); + $lockJsonMock->expects($this->atLeastOnce()) + ->method('write') + ->will($this->returnCallback(function ($hash, $options) use (&$actualLock) { + // need to do assertion outside of mock for nice phpunit output + // so store value temporarily in reference for later assetion + $actualLock = $hash; + })); + } - $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); - $autoloadGenerator = $this->getMock('Composer\Autoload\AutoloadGenerator', array(), array($eventDispatcher)); - $composer->setAutoloadGenerator($autoloadGenerator); - $composer->setEventDispatcher($eventDispatcher); - - $installer = Installer::create($io, $composer); - - $application = new Application; - $application->get('install')->setCode(function ($input, $output) use ($installer) { - $installer - ->setDevMode(!$input->getOption('no-dev')) - ->setDryRun($input->getOption('dry-run')) - ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')); - - return $installer->run(); - }); - - $application->get('update')->setCode(function ($input, $output) use ($installer) { - $installer - ->setDevMode(!$input->getOption('no-dev')) - ->setUpdate(true) - ->setDryRun($input->getOption('dry-run')) - ->setUpdateWhitelist($input->getArgument('packages')) - ->setWhitelistDependencies($input->getOption('with-dependencies')) - ->setPreferStable($input->getOption('prefer-stable')) - ->setPreferLowest($input->getOption('prefer-lowest')) - ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')); - - return $installer->run(); - }); - - if (!preg_match('{^(install|update)\b}', $run)) { - throw new \UnexpectedValueException('The run command only supports install and update'); - } + $contents = json_encode($composerConfig); + $locker = new Locker($io, $lockJsonMock, $repositoryManager, $composer->getInstallationManager(), $contents); + $composer->setLocker($locker); + + $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); + $autoloadGenerator = $this->getMock('Composer\Autoload\AutoloadGenerator', array(), array($eventDispatcher)); + $composer->setAutoloadGenerator($autoloadGenerator); + $composer->setEventDispatcher($eventDispatcher); + + $installer = Installer::create($io, $composer); + + $application = new Application; + $application->get('install')->setCode(function ($input, $output) use ($installer) { + $installer + ->setDevMode(!$input->getOption('no-dev')) + ->setDryRun($input->getOption('dry-run')) + ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')); + + return $installer->run(); + }); + + $application->get('update')->setCode(function ($input, $output) use ($installer) { + $installer + ->setDevMode(!$input->getOption('no-dev')) + ->setUpdate(true) + ->setDryRun($input->getOption('dry-run')) + ->setUpdateWhitelist($input->getArgument('packages')) + ->setWhitelistDependencies($input->getOption('with-dependencies')) + ->setPreferStable($input->getOption('prefer-stable')) + ->setPreferLowest($input->getOption('prefer-lowest')) + ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')); + + return $installer->run(); + }); + + if (!preg_match('{^(install|update)\b}', $run)) { + throw new \UnexpectedValueException('The run command only supports install and update'); + } - $application->setAutoExit(false); - $appOutput = fopen('php://memory', 'w+'); - $result = $application->run(new StringInput($run), new StreamOutput($appOutput)); - fseek($appOutput, 0); - $this->assertEquals($expectExitCode, $result, $output . stream_get_contents($appOutput)); - - if ($expectLock) { - unset($actualLock['hash']); - unset($actualLock['content-hash']); - unset($actualLock['_readme']); - $this->assertEquals($expectLock, $actualLock); - } + $application->setAutoExit(false); + $appOutput = fopen('php://memory', 'w+'); + $result = $application->run(new StringInput($run), new StreamOutput($appOutput)); + fseek($appOutput, 0); + $this->assertEquals($expectExitCode, $result, $output . stream_get_contents($appOutput)); + + if ($expectLock) { + unset($actualLock['hash']); + unset($actualLock['content-hash']); + unset($actualLock['_readme']); + $this->assertEquals($expectLock, $actualLock); + } - $installationManager = $composer->getInstallationManager(); - $this->assertSame(rtrim($expect), implode("\n", $installationManager->getTrace())); + $installationManager = $composer->getInstallationManager(); + $this->assertSame(rtrim($expect), implode("\n", $installationManager->getTrace())); - if ($expectOutput) { - $this->assertEquals(rtrim($expectOutput), rtrim($output)); + if ($expectOutput) { + $this->assertEquals(rtrim($expectOutput), rtrim($output)); + } + } + catch(\Exception $e) { + // Exception was thrown during execution + if (!$expect || !$expectOutput) { + throw $e; + } + $this->assertEquals('EXCEPTION', rtrim($expect)); + $normalizedOutput = rtrim(str_replace("\n", PHP_EOL, $expectOutput)); + $this->assertEquals($normalizedOutput, rtrim($e->getMessage())); } + return; } public function getIntegrationTests() From bd241cb896a2205ec651e1651e942650d9ad59b8 Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Wed, 27 Jan 2016 10:05:10 +0100 Subject: [PATCH 81/98] Included unit test for circular root dependencies. --- .../installer/install-self-from-root.test | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/Composer/Test/Fixtures/installer/install-self-from-root.test diff --git a/tests/Composer/Test/Fixtures/installer/install-self-from-root.test b/tests/Composer/Test/Fixtures/installer/install-self-from-root.test new file mode 100644 index 000000000..35a50754a --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/install-self-from-root.test @@ -0,0 +1,16 @@ +--TEST-- +Tries to require a package with the same name as the root package +--COMPOSER-- +{ + "name": "foo/bar", + "require": { + "foo/bar": "@dev" + } +} +--RUN-- +install +--EXPECT-OUTPUT-- +Root package 'foo/bar' cannot require itself in its composer.json +Did you accidentally name your root package after an external package? +--EXPECT-- +EXCEPTION From 639ee0701c86b7b38fe90b091358a063ce0f2d99 Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Wed, 27 Jan 2016 13:19:08 +0100 Subject: [PATCH 82/98] Introduced more generic, less invasive way to test for exceptions in fixtures, more in line with how phpunit works. --- .../installer/install-self-from-root.test | 6 +- tests/Composer/Test/InstallerTest.php | 194 +++++++++--------- 2 files changed, 100 insertions(+), 100 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/install-self-from-root.test b/tests/Composer/Test/Fixtures/installer/install-self-from-root.test index 35a50754a..ceeef8b7c 100644 --- a/tests/Composer/Test/Fixtures/installer/install-self-from-root.test +++ b/tests/Composer/Test/Fixtures/installer/install-self-from-root.test @@ -9,8 +9,8 @@ Tries to require a package with the same name as the root package } --RUN-- install ---EXPECT-OUTPUT-- +--EXPECT-EXIT-CODE-- +InvalidArgumentException +--EXPECT-- Root package 'foo/bar' cannot require itself in its composer.json Did you accidentally name your root package after an external package? ---EXPECT-- -EXCEPTION diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index f72c6d9d5..518a395e1 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -159,108 +159,108 @@ class InstallerTest extends TestCase ->will($this->returnCallback($callback)); // Prepare for exceptions - try { - $composer = FactoryMock::create($io, $composerConfig); - - $jsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); - $jsonMock->expects($this->any()) - ->method('read') - ->will($this->returnValue($installed)); - $jsonMock->expects($this->any()) - ->method('exists') - ->will($this->returnValue(true)); - - $repositoryManager = $composer->getRepositoryManager(); - $repositoryManager->setLocalRepository(new InstalledFilesystemRepositoryMock($jsonMock)); - - $lockJsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); - $lockJsonMock->expects($this->any()) - ->method('read') - ->will($this->returnValue($lock)); - $lockJsonMock->expects($this->any()) - ->method('exists') - ->will($this->returnValue(true)); - - if ($expectLock) { - $actualLock = array(); - $lockJsonMock->expects($this->atLeastOnce()) - ->method('write') - ->will($this->returnCallback(function ($hash, $options) use (&$actualLock) { - // need to do assertion outside of mock for nice phpunit output - // so store value temporarily in reference for later assetion - $actualLock = $hash; - })); - } + if (is_int($expectExitCode) || ctype_digit($expectExitCode)) { + $expectExitCode = (int) $expectExitCode; + } else { + $normalizedOutput = rtrim(str_replace("\n", PHP_EOL, $expect)); + $this->setExpectedException($expectExitCode, $normalizedOutput); + } - $contents = json_encode($composerConfig); - $locker = new Locker($io, $lockJsonMock, $repositoryManager, $composer->getInstallationManager(), $contents); - $composer->setLocker($locker); - - $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); - $autoloadGenerator = $this->getMock('Composer\Autoload\AutoloadGenerator', array(), array($eventDispatcher)); - $composer->setAutoloadGenerator($autoloadGenerator); - $composer->setEventDispatcher($eventDispatcher); - - $installer = Installer::create($io, $composer); - - $application = new Application; - $application->get('install')->setCode(function ($input, $output) use ($installer) { - $installer - ->setDevMode(!$input->getOption('no-dev')) - ->setDryRun($input->getOption('dry-run')) - ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')); - - return $installer->run(); - }); - - $application->get('update')->setCode(function ($input, $output) use ($installer) { - $installer - ->setDevMode(!$input->getOption('no-dev')) - ->setUpdate(true) - ->setDryRun($input->getOption('dry-run')) - ->setUpdateWhitelist($input->getArgument('packages')) - ->setWhitelistDependencies($input->getOption('with-dependencies')) - ->setPreferStable($input->getOption('prefer-stable')) - ->setPreferLowest($input->getOption('prefer-lowest')) - ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')); - - return $installer->run(); - }); - - if (!preg_match('{^(install|update)\b}', $run)) { - throw new \UnexpectedValueException('The run command only supports install and update'); - } + // Create Composer mock object according to configuration + $composer = FactoryMock::create($io, $composerConfig); + + $jsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); + $jsonMock->expects($this->any()) + ->method('read') + ->will($this->returnValue($installed)); + $jsonMock->expects($this->any()) + ->method('exists') + ->will($this->returnValue(true)); + + $repositoryManager = $composer->getRepositoryManager(); + $repositoryManager->setLocalRepository(new InstalledFilesystemRepositoryMock($jsonMock)); + + $lockJsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); + $lockJsonMock->expects($this->any()) + ->method('read') + ->will($this->returnValue($lock)); + $lockJsonMock->expects($this->any()) + ->method('exists') + ->will($this->returnValue(true)); + + if ($expectLock) { + $actualLock = array(); + $lockJsonMock->expects($this->atLeastOnce()) + ->method('write') + ->will($this->returnCallback(function ($hash, $options) use (&$actualLock) { + // need to do assertion outside of mock for nice phpunit output + // so store value temporarily in reference for later assetion + $actualLock = $hash; + })); + } - $application->setAutoExit(false); - $appOutput = fopen('php://memory', 'w+'); - $result = $application->run(new StringInput($run), new StreamOutput($appOutput)); - fseek($appOutput, 0); - $this->assertEquals($expectExitCode, $result, $output . stream_get_contents($appOutput)); - - if ($expectLock) { - unset($actualLock['hash']); - unset($actualLock['content-hash']); - unset($actualLock['_readme']); - $this->assertEquals($expectLock, $actualLock); - } + $contents = json_encode($composerConfig); + $locker = new Locker($io, $lockJsonMock, $repositoryManager, $composer->getInstallationManager(), $contents); + $composer->setLocker($locker); - $installationManager = $composer->getInstallationManager(); - $this->assertSame(rtrim($expect), implode("\n", $installationManager->getTrace())); + $eventDispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); + $autoloadGenerator = $this->getMock('Composer\Autoload\AutoloadGenerator', array(), array($eventDispatcher)); + $composer->setAutoloadGenerator($autoloadGenerator); + $composer->setEventDispatcher($eventDispatcher); + + $installer = Installer::create($io, $composer); + + $application = new Application; + $application->get('install')->setCode(function ($input, $output) use ($installer) { + $installer + ->setDevMode(!$input->getOption('no-dev')) + ->setDryRun($input->getOption('dry-run')) + ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')); + + return $installer->run(); + }); + + $application->get('update')->setCode(function ($input, $output) use ($installer) { + $installer + ->setDevMode(!$input->getOption('no-dev')) + ->setUpdate(true) + ->setDryRun($input->getOption('dry-run')) + ->setUpdateWhitelist($input->getArgument('packages')) + ->setWhitelistDependencies($input->getOption('with-dependencies')) + ->setPreferStable($input->getOption('prefer-stable')) + ->setPreferLowest($input->getOption('prefer-lowest')) + ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')); + + return $installer->run(); + }); + + if (!preg_match('{^(install|update)\b}', $run)) { + throw new \UnexpectedValueException('The run command only supports install and update'); + } - if ($expectOutput) { - $this->assertEquals(rtrim($expectOutput), rtrim($output)); - } + $application->setAutoExit(false); + $appOutput = fopen('php://memory', 'w+'); + $result = $application->run(new StringInput($run), new StreamOutput($appOutput)); + fseek($appOutput, 0); + if (!is_int($expectExitCode)) { + // Shouldn't check output and results if an exception was expected by this point + return; } - catch(\Exception $e) { - // Exception was thrown during execution - if (!$expect || !$expectOutput) { - throw $e; - } - $this->assertEquals('EXCEPTION', rtrim($expect)); - $normalizedOutput = rtrim(str_replace("\n", PHP_EOL, $expectOutput)); - $this->assertEquals($normalizedOutput, rtrim($e->getMessage())); + + $this->assertEquals($expectExitCode, $result, $output . stream_get_contents($appOutput)); + if ($expectLock) { + unset($actualLock['hash']); + unset($actualLock['content-hash']); + unset($actualLock['_readme']); + $this->assertEquals($expectLock, $actualLock); + } + + $installationManager = $composer->getInstallationManager(); + $this->assertSame(rtrim($expect), implode("\n", $installationManager->getTrace())); + + if ($expectOutput) { + $this->assertEquals(rtrim($expectOutput), rtrim($output)); } - return; } public function getIntegrationTests() @@ -316,7 +316,7 @@ class InstallerTest extends TestCase } $expectOutput = isset($testData['EXPECT-OUTPUT']) ? $testData['EXPECT-OUTPUT'] : null; $expect = $testData['EXPECT']; - $expectExitCode = isset($testData['EXPECT-EXIT-CODE']) ? (int) $testData['EXPECT-EXIT-CODE'] : 0; + $expectExitCode = isset($testData['EXPECT-EXIT-CODE']) ? $testData['EXPECT-EXIT-CODE'] : 0; } catch (\Exception $e) { die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file))); } From 523362c7c5a5058174de6389baebf1d82078c73f Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Wed, 27 Jan 2016 13:46:14 +0100 Subject: [PATCH 83/98] Cleaner notation for expected exceptions in fixtures. --- .../installer/install-self-from-root.test | 2 +- tests/Composer/Test/InstallerTest.php | 28 +++++++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/install-self-from-root.test b/tests/Composer/Test/Fixtures/installer/install-self-from-root.test index ceeef8b7c..82092c77f 100644 --- a/tests/Composer/Test/Fixtures/installer/install-self-from-root.test +++ b/tests/Composer/Test/Fixtures/installer/install-self-from-root.test @@ -9,7 +9,7 @@ Tries to require a package with the same name as the root package } --RUN-- install ---EXPECT-EXIT-CODE-- +--EXPECT-EXCEPTION-- InvalidArgumentException --EXPECT-- Root package 'foo/bar' cannot require itself in its composer.json diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 518a395e1..a50563280 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -137,7 +137,7 @@ class InstallerTest extends TestCase /** * @dataProvider getIntegrationTests */ - public function testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectExitCode) + public function testIntegration($file, $message, $condition, $composerConfig, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectResult) { if ($condition) { eval('$res = '.$condition.';'); @@ -159,11 +159,11 @@ class InstallerTest extends TestCase ->will($this->returnCallback($callback)); // Prepare for exceptions - if (is_int($expectExitCode) || ctype_digit($expectExitCode)) { - $expectExitCode = (int) $expectExitCode; + if (is_int($expectResult) || ctype_digit($expectResult)) { + $expectResult = (int) $expectResult; } else { $normalizedOutput = rtrim(str_replace("\n", PHP_EOL, $expect)); - $this->setExpectedException($expectExitCode, $normalizedOutput); + $this->setExpectedException($expectResult, $normalizedOutput); } // Create Composer mock object according to configuration @@ -242,12 +242,12 @@ class InstallerTest extends TestCase $appOutput = fopen('php://memory', 'w+'); $result = $application->run(new StringInput($run), new StreamOutput($appOutput)); fseek($appOutput, 0); - if (!is_int($expectExitCode)) { + if (!is_int($expectResult)) { // Shouldn't check output and results if an exception was expected by this point return; } - $this->assertEquals($expectExitCode, $result, $output . stream_get_contents($appOutput)); + $this->assertEquals($expectResult, $result, $output . stream_get_contents($appOutput)); if ($expectLock) { unset($actualLock['hash']); unset($actualLock['content-hash']); @@ -279,7 +279,7 @@ class InstallerTest extends TestCase $installedDev = array(); $lock = array(); $expectLock = array(); - $expectExitCode = 0; + $expectResult = 0; try { $message = $testData['TEST']; @@ -316,12 +316,21 @@ class InstallerTest extends TestCase } $expectOutput = isset($testData['EXPECT-OUTPUT']) ? $testData['EXPECT-OUTPUT'] : null; $expect = $testData['EXPECT']; - $expectExitCode = isset($testData['EXPECT-EXIT-CODE']) ? $testData['EXPECT-EXIT-CODE'] : 0; + if (!empty($testData['EXPECT-EXCEPTION'])) { + $expectResult = $testData['EXPECT-EXCEPTION']; + if (!empty($testData['EXPECT-EXIT-CODE'])) { + throw new \LogicException('EXPECT-EXCEPTION and EXPECT-EXIT-CODE are mutually exclusive'); + } + } elseif (!empty($testData['EXPECT-EXIT-CODE'])) { + $expectResult = (int) $testData['EXPECT-EXIT-CODE']; + } else { + $expectResult = 0; + } } catch (\Exception $e) { die(sprintf('Test "%s" is not valid: '.$e->getMessage(), str_replace($fixturesDir.'/', '', $file))); } - $tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectExitCode); + $tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectResult); } return $tests; @@ -341,6 +350,7 @@ class InstallerTest extends TestCase 'EXPECT-LOCK' => false, 'EXPECT-OUTPUT' => false, 'EXPECT-EXIT-CODE' => false, + 'EXPECT-EXCEPTION' => false, 'EXPECT' => true, ); From 3e06c801f4a2c0da9d70dad55e05e9fe28eebc8d Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Wed, 27 Jan 2016 13:49:52 +0100 Subject: [PATCH 84/98] Cleaned up check+conversion that was no longer required. --- tests/Composer/Test/InstallerTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index a50563280..4f599bf21 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -159,9 +159,7 @@ class InstallerTest extends TestCase ->will($this->returnCallback($callback)); // Prepare for exceptions - if (is_int($expectResult) || ctype_digit($expectResult)) { - $expectResult = (int) $expectResult; - } else { + if (!is_int($expectResult)) { $normalizedOutput = rtrim(str_replace("\n", PHP_EOL, $expect)); $this->setExpectedException($expectResult, $normalizedOutput); } From 8d57c3e743b61f3aec017687085291acc146d647 Mon Sep 17 00:00:00 2001 From: Jefferson Carpenter Date: Thu, 28 Jan 2016 19:56:25 -0600 Subject: [PATCH 85/98] Update SolverProblemsException.php --- src/Composer/DependencyResolver/SolverProblemsException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index a457c5c63..c6092c28c 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -41,7 +41,7 @@ class SolverProblemsException extends \RuntimeException } if (strpos($text, 'could not be found') || strpos($text, 'no matching package found')) { - $text .= "\nPotential causes:\n - A typo in the package name\n - The package is not available in a stable-enough version according to your minimum-stability setting\n see for more details.\n\nRead for further common problems."; + $text .= "\nPotential causes:\n - A typo in the package name\n - The package is not available in a stable-enough version according to your minimum-stability setting\n see for more details.\n\nRead for further common problems."; } if ($hasExtensionProblems) { From 087b901545194efdbb17544e6a3458479aca3657 Mon Sep 17 00:00:00 2001 From: Jefferson Carpenter Date: Thu, 28 Jan 2016 20:12:51 -0600 Subject: [PATCH 86/98] Update broken-deps-do-not-replace.test --- .../Test/Fixtures/installer/broken-deps-do-not-replace.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test index 19bd8f914..d8b09c4a1 100644 --- a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test +++ b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test @@ -33,7 +33,7 @@ install Potential causes: - A typo in the package name - The package is not available in a stable-enough version according to your minimum-stability setting - see for more details. + see for more details. Read for further common problems. From b7845bb6c0f0d263dee75e3edf565cbc897ac5ea Mon Sep 17 00:00:00 2001 From: Jefferson Carpenter Date: Thu, 28 Jan 2016 20:13:44 -0600 Subject: [PATCH 87/98] Update SolverTest.php --- tests/Composer/Test/DependencyResolver/SolverTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index ed62e3d79..63de2973f 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -709,7 +709,7 @@ class SolverTest extends TestCase $msg .= "Potential causes:\n"; $msg .= " - A typo in the package name\n"; $msg .= " - The package is not available in a stable-enough version according to your minimum-stability setting\n"; - $msg .= " see for more details.\n\n"; + $msg .= " see for more details.\n\n"; $msg .= "Read for further common problems."; $this->assertEquals($msg, $e->getMessage()); } From 49d7d65933c78e163354c2c580883926f1c8daee Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 28 Jan 2016 13:41:19 +0000 Subject: [PATCH 88/98] Add verbosity input support to IOInterface --- src/Composer/Cache.php | 16 ++---- src/Composer/Command/ShowCommand.php | 18 +++--- src/Composer/Console/Application.php | 14 ++--- src/Composer/Downloader/ArchiveDownloader.php | 5 +- src/Composer/Downloader/FileDownloader.php | 4 +- .../EventDispatcher/EventDispatcher.php | 4 +- src/Composer/Factory.php | 12 +--- src/Composer/IO/BufferIO.php | 2 +- src/Composer/IO/ConsoleIO.php | 49 ++++++++++------ src/Composer/IO/IOInterface.php | 38 ++++++++----- src/Composer/IO/NullIO.php | 8 +-- src/Composer/Installer.php | 12 ++-- src/Composer/Installer/PearInstaller.php | 4 +- src/Composer/Plugin/PluginManager.php | 4 +- .../Repository/ArtifactRepository.php | 10 +--- src/Composer/Repository/PearRepository.php | 4 +- .../Repository/Vcs/GitBitbucketDriver.php | 4 +- src/Composer/Repository/Vcs/GitHubDriver.php | 4 +- src/Composer/Repository/Vcs/GitLabDriver.php | 4 +- .../Repository/Vcs/HgBitbucketDriver.php | 4 +- src/Composer/Util/RemoteFilesystem.php | 24 +++----- tests/Composer/Test/ApplicationTest.php | 9 +++ .../EventDispatcher/EventDispatcherTest.php | 57 +++++-------------- .../Fixtures/installer/abandoned-listed.test | 8 +-- .../installer/broken-deps-do-not-replace.test | 6 +- .../Fixtures/installer/suggest-installed.test | 8 +-- .../Test/Fixtures/installer/suggest-prod.test | 8 +-- .../Fixtures/installer/suggest-replaced.test | 8 +-- .../installer/suggest-uninstalled.test | 8 +-- tests/Composer/Test/IO/ConsoleIOTest.php | 27 ++++++--- tests/Composer/Test/InstallerTest.php | 17 ++---- 31 files changed, 181 insertions(+), 219 deletions(-) diff --git a/src/Composer/Cache.php b/src/Composer/Cache.php index 8c5bce4ee..7090a8a0d 100644 --- a/src/Composer/Cache.php +++ b/src/Composer/Cache.php @@ -67,9 +67,7 @@ class Cache { $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); if ($this->enabled && file_exists($this->root . $file)) { - if ($this->io->isDebug()) { - $this->io->writeError('Reading '.$this->root . $file.' from cache'); - } + $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG); return file_get_contents($this->root . $file); } @@ -82,16 +80,12 @@ class Cache if ($this->enabled) { $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); - if ($this->io->isDebug()) { - $this->io->writeError('Writing '.$this->root . $file.' into cache'); - } + $this->io->writeError('Writing '.$this->root . $file.' into cache', true, IOInterface::DEBUG); try { return file_put_contents($this->root . $file, $contents); } catch (\ErrorException $e) { - if ($this->io->isDebug()) { - $this->io->writeError('Failed to write into cache: '.$e->getMessage().''); - } + $this->io->writeError('Failed to write into cache: '.$e->getMessage().'', true, IOInterface::DEBUG); if (preg_match('{^file_put_contents\(\): Only ([0-9]+) of ([0-9]+) bytes written}', $e->getMessage(), $m)) { // Remove partial file. unlink($this->root . $file); @@ -152,9 +146,7 @@ class Cache touch($this->root . $file); } - if ($this->io->isDebug()) { - $this->io->writeError('Reading '.$this->root . $file.' from cache'); - } + $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG); return copy($this->root . $file, $target); } diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 7f0c77537..e3e1ffd71 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -246,10 +246,10 @@ EOT $writeDescription = !$input->getOption('name-only') && !$input->getOption('path') && ($nameLength + ($showVersion ? $versionLength : 0) + 24 <= $width); foreach ($packages[$type] as $package) { if (is_object($package)) { - $output->write($indent . str_pad($package->getPrettyName(), $nameLength, ' '), false); + $io->write($indent . str_pad($package->getPrettyName(), $nameLength, ' '), false); if ($writeVersion) { - $output->write(' ' . str_pad($package->getFullPrettyVersion(), $versionLength, ' '), false); + $io->write(' ' . str_pad($package->getFullPrettyVersion(), $versionLength, ' '), false); } if ($writeDescription) { @@ -258,15 +258,15 @@ EOT if (strlen($description) > $remaining) { $description = substr($description, 0, $remaining - 3) . '...'; } - $output->write(' ' . $description); + $io->write(' ' . $description, false); } if ($writePath) { $path = strtok(realpath($composer->getInstallationManager()->getInstallPath($package)), "\r\n"); - $output->write(' ' . $path); + $io->write(' ' . $path, false); } } else { - $output->write($indent . $package); + $io->write($indent . $package, false); } $io->write(''); } @@ -489,10 +489,10 @@ EOT $packagesInTree = array(); $packagesInTree[] = $package; - $output->write(sprintf('%s', $package->getPrettyName())); - $output->write(' ' . $package->getPrettyVersion()); - $output->write(' ' . strtok($package->getDescription(), "\r\n")); - $output->writeln(''); + $io = $this->getIO(); + $io->write(sprintf('%s', $package->getPrettyName()), false); + $io->write(' ' . $package->getPrettyVersion(), false); + $io->write(' ' . strtok($package->getDescription(), "\r\n")); if (is_object($package)) { $requires = $package->getRequires(); diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 632741586..f77876100 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -136,9 +136,7 @@ class Application extends BaseApplication if ($newWorkDir = $this->getNewWorkingDir($input)) { $oldWorkingDir = getcwd(); chdir($newWorkDir); - if ($io->isDebug() >= 4) { - $io->writeError('Changed CWD to ' . getcwd()); - } + $io->writeError('Changed CWD to ' . getcwd(), true, IOInterface::DEBUG); } // add non-standard scripts as own commands @@ -214,7 +212,7 @@ class Application extends BaseApplication || (($df = disk_free_space($dir = $config->get('vendor-dir'))) !== false && $df < $minSpaceFree) || (($df = disk_free_space($dir = sys_get_temp_dir())) !== false && $df < $minSpaceFree) ) { - $io->writeError('The disk hosting '.$dir.' is full, this may be the cause of the following exception'); + $io->writeError('The disk hosting '.$dir.' is full, this may be the cause of the following exception', true, IOInterface::QUIET); } } } catch (\Exception $e) { @@ -222,13 +220,13 @@ class Application extends BaseApplication Silencer::restore(); if (defined('PHP_WINDOWS_VERSION_BUILD') && false !== strpos($exception->getMessage(), 'The system cannot find the path specified')) { - $io->writeError('The following exception may be caused by a stale entry in your cmd.exe AutoRun'); - $io->writeError('Check https://getcomposer.org/doc/articles/troubleshooting.md#-the-system-cannot-find-the-path-specified-windows- for details'); + $io->writeError('The following exception may be caused by a stale entry in your cmd.exe AutoRun', true, IOInterface::QUIET); + $io->writeError('Check https://getcomposer.org/doc/articles/troubleshooting.md#-the-system-cannot-find-the-path-specified-windows- for details', true, IOInterface::QUIET); } if (false !== strpos($exception->getMessage(), 'fork failed - Cannot allocate memory')) { - $io->writeError('The following exception is caused by a lack of memory and not having swap configured'); - $io->writeError('Check https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors for details'); + $io->writeError('The following exception is caused by a lack of memory and not having swap configured', true, IOInterface::QUIET); + $io->writeError('Check https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors for details', true, IOInterface::QUIET); } } diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index f42b82872..9fa6a3338 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -14,6 +14,7 @@ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Symfony\Component\Finder\Finder; +use Composer\IO\IOInterface; /** * Base downloader for archives @@ -34,9 +35,7 @@ abstract class ArchiveDownloader extends FileDownloader while ($retries--) { $fileName = parent::download($package, $path); - if ($this->io->isVerbose()) { - $this->io->writeError(' Extracting archive'); - } + $this->io->writeError(' Extracting archive', true, IOInterface::VERBOSE); try { $this->filesystem->ensureDirectoryExists($temporaryDir); diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index d3a57500c..ece495dba 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -141,9 +141,7 @@ class FileDownloader implements DownloaderInterface if ((0 !== $e->getCode() && !in_array($e->getCode(), array(500, 502, 503, 504))) || !$retries) { throw $e; } - if ($this->io->isVerbose()) { - $this->io->writeError(' Download failed, retrying...'); - } + $this->io->writeError(' Download failed, retrying...', true, IOInterface::VERBOSE); usleep(500000); } } diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index c669b9dab..b9a879a01 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -155,9 +155,7 @@ class EventDispatcher $event = $this->checkListenerExpectedEvent($callable, $event); $return = false === call_user_func($callable, $event) ? 1 : 0; } elseif ($this->isComposerScript($callable)) { - if ($this->io->isVerbose()) { - $this->io->writeError(sprintf('> %s: %s', $event->getName(), $callable)); - } + $this->io->writeError(sprintf('> %s: %s', $event->getName(), $callable), true, IOInterface::VERBOSE); $scriptName = substr($callable, 1); $args = $event->getArguments(); $flags = $event->getFlags(); diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index e11677b56..989975fde 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -293,14 +293,10 @@ class Factory $config = static::createConfig($io, $cwd); $config->merge($localConfig); if (isset($composerFile)) { - if ($io && $io->isDebug()) { - $io->writeError('Loading config file ' . $composerFile); - } + $io->writeError('Loading config file ' . $composerFile, true, IOInterface::DEBUG); $localAuthFile = new JsonFile(dirname(realpath($composerFile)) . '/auth.json'); if ($localAuthFile->exists()) { - if ($io && $io->isDebug()) { - $io->writeError('Loading config file ' . $localAuthFile->getPath()); - } + $io->writeError('Loading config file ' . $localAuthFile->getPath(), true, IOInterface::DEBUG); $config->merge(array('config' => $localAuthFile->read())); $config->setAuthConfigSource(new JsonConfigSource($localAuthFile, true)); } @@ -435,9 +431,7 @@ class Factory try { $composer = self::createComposer($io, $config->get('home') . '/composer.json', $disablePlugins, $config->get('home'), false); } catch (\Exception $e) { - if ($io->isDebug()) { - $io->writeError('Failed to initialize global composer: '.$e->getMessage()); - } + $io->writeError('Failed to initialize global composer: '.$e->getMessage(), true, IOInterface::DEBUG); } return $composer; diff --git a/src/Composer/IO/BufferIO.php b/src/Composer/IO/BufferIO.php index db3fb634b..1069c0d9a 100644 --- a/src/Composer/IO/BufferIO.php +++ b/src/Composer/IO/BufferIO.php @@ -35,7 +35,7 @@ class BufferIO extends ConsoleIO $input = new StringInput($input); $input->setInteractive(false); - $output = new StreamOutput(fopen('php://memory', 'rw'), $verbosity, !empty($formatter), $formatter); + $output = new StreamOutput(fopen('php://memory', 'rw'), $verbosity, $formatter ? $formatter->isDecorated() : false, $formatter); parent::__construct($input, $output, new HelperSet(array())); } diff --git a/src/Composer/IO/ConsoleIO.php b/src/Composer/IO/ConsoleIO.php index 3867695f1..f97af2e8a 100644 --- a/src/Composer/IO/ConsoleIO.php +++ b/src/Composer/IO/ConsoleIO.php @@ -33,6 +33,7 @@ class ConsoleIO extends BaseIO protected $lastMessage; protected $lastMessageErr; private $startTime; + private $verbosityMap; /** * Constructor. @@ -46,6 +47,13 @@ class ConsoleIO extends BaseIO $this->input = $input; $this->output = $output; $this->helperSet = $helperSet; + $this->verbosityMap = array( + self::QUIET => OutputInterface::VERBOSITY_QUIET, + self::NORMAL => OutputInterface::VERBOSITY_NORMAL, + self::VERBOSE => OutputInterface::VERBOSITY_VERBOSE, + self::VERY_VERBOSE => OutputInterface::VERBOSITY_VERY_VERBOSE, + self::DEBUG => OutputInterface::VERBOSITY_DEBUG, + ); } public function enableDebugging($startTime) @@ -96,26 +104,32 @@ class ConsoleIO extends BaseIO /** * {@inheritDoc} */ - public function write($messages, $newline = true) + public function write($messages, $newline = true, $verbosity = self::NORMAL) { - $this->doWrite($messages, $newline, false); + $this->doWrite($messages, $newline, false, $verbosity); } /** * {@inheritDoc} */ - public function writeError($messages, $newline = true) + public function writeError($messages, $newline = true, $verbosity = self::NORMAL) { - $this->doWrite($messages, $newline, true); + $this->doWrite($messages, $newline, true, $verbosity); } /** * @param array|string $messages * @param bool $newline * @param bool $stderr + * @param int $verbosity */ - private function doWrite($messages, $newline, $stderr) + private function doWrite($messages, $newline, $stderr, $verbosity) { + $sfVerbosity = $this->verbosityMap[$verbosity]; + if ($sfVerbosity > $this->output->getVerbosity()) { + return; + } + if (null !== $this->startTime) { $memoryUsage = memory_get_usage() / 1024 / 1024; $timeSpent = microtime(true) - $this->startTime; @@ -125,30 +139,30 @@ class ConsoleIO extends BaseIO } if (true === $stderr && $this->output instanceof ConsoleOutputInterface) { - $this->output->getErrorOutput()->write($messages, $newline); + $this->output->getErrorOutput()->write($messages, $newline, $sfVerbosity); $this->lastMessageErr = join($newline ? "\n" : '', (array) $messages); return; } - $this->output->write($messages, $newline); + $this->output->write($messages, $newline, $sfVerbosity); $this->lastMessage = join($newline ? "\n" : '', (array) $messages); } /** * {@inheritDoc} */ - public function overwrite($messages, $newline = true, $size = null) + public function overwrite($messages, $newline = true, $size = null, $verbosity = self::NORMAL) { - $this->doOverwrite($messages, $newline, $size, false); + $this->doOverwrite($messages, $newline, $size, false, $verbosity); } /** * {@inheritDoc} */ - public function overwriteError($messages, $newline = true, $size = null) + public function overwriteError($messages, $newline = true, $size = null, $verbosity = self::NORMAL) { - $this->doOverwrite($messages, $newline, $size, true); + $this->doOverwrite($messages, $newline, $size, true, $verbosity); } /** @@ -156,8 +170,9 @@ class ConsoleIO extends BaseIO * @param bool $newline * @param int|null $size * @param bool $stderr + * @param int $verbosity */ - private function doOverwrite($messages, $newline, $size, $stderr) + private function doOverwrite($messages, $newline, $size, $stderr, $verbosity) { // messages can be an array, let's convert it to string anyway $messages = join($newline ? "\n" : '', (array) $messages); @@ -168,21 +183,21 @@ class ConsoleIO extends BaseIO $size = strlen(strip_tags($stderr ? $this->lastMessageErr : $this->lastMessage)); } // ...let's fill its length with backspaces - $this->doWrite(str_repeat("\x08", $size), false, $stderr); + $this->doWrite(str_repeat("\x08", $size), false, $stderr, $verbosity); // write the new message - $this->doWrite($messages, false, $stderr); + $this->doWrite($messages, false, $stderr, $verbosity); $fill = $size - strlen(strip_tags($messages)); if ($fill > 0) { // whitespace whatever has left - $this->doWrite(str_repeat(' ', $fill), false, $stderr); + $this->doWrite(str_repeat(' ', $fill), false, $stderr, $verbosity); // move the cursor back - $this->doWrite(str_repeat("\x08", $fill), false, $stderr); + $this->doWrite(str_repeat("\x08", $fill), false, $stderr, $verbosity); } if ($newline) { - $this->doWrite('', true, $stderr); + $this->doWrite('', true, $stderr, $verbosity); } if ($stderr) { diff --git a/src/Composer/IO/IOInterface.php b/src/Composer/IO/IOInterface.php index 3165b0d24..ff20a591d 100644 --- a/src/Composer/IO/IOInterface.php +++ b/src/Composer/IO/IOInterface.php @@ -21,6 +21,12 @@ use Composer\Config; */ interface IOInterface { + const QUIET = 1; + const NORMAL = 2; + const VERBOSE = 4; + const VERY_VERBOSE = 8; + const DEBUG = 16; + /** * Is this input means interactive? * @@ -59,36 +65,40 @@ interface IOInterface /** * Writes a message to the output. * - * @param string|array $messages The message as an array of lines or a single string - * @param bool $newline Whether to add a newline or not + * @param string|array $messages The message as an array of lines or a single string + * @param bool $newline Whether to add a newline or not + * @param int $verbosity Verbosity level from the VERBOSITY_* constants */ - public function write($messages, $newline = true); + public function write($messages, $newline = true, $verbosity = self::NORMAL); /** * Writes a message to the error output. * - * @param string|array $messages The message as an array of lines or a single string - * @param bool $newline Whether to add a newline or not + * @param string|array $messages The message as an array of lines or a single string + * @param bool $newline Whether to add a newline or not + * @param int $verbosity Verbosity level from the VERBOSITY_* constants */ - public function writeError($messages, $newline = true); + public function writeError($messages, $newline = true, $verbosity = self::NORMAL); /** * Overwrites a previous message to the output. * - * @param string|array $messages The message as an array of lines or a single string - * @param bool $newline Whether to add a newline or not - * @param int $size The size of line + * @param string|array $messages The message as an array of lines or a single string + * @param bool $newline Whether to add a newline or not + * @param int $size The size of line + * @param int $verbosity Verbosity level from the VERBOSITY_* constants */ - public function overwrite($messages, $newline = true, $size = null); + public function overwrite($messages, $newline = true, $size = null, $verbosity = self::NORMAL); /** * Overwrites a previous message to the error output. * - * @param string|array $messages The message as an array of lines or a single string - * @param bool $newline Whether to add a newline or not - * @param int $size The size of line + * @param string|array $messages The message as an array of lines or a single string + * @param bool $newline Whether to add a newline or not + * @param int $size The size of line + * @param int $verbosity Verbosity level from the VERBOSITY_* constants */ - public function overwriteError($messages, $newline = true, $size = null); + public function overwriteError($messages, $newline = true, $size = null, $verbosity = self::NORMAL); /** * Asks a question to the user. diff --git a/src/Composer/IO/NullIO.php b/src/Composer/IO/NullIO.php index 1a88395d3..587168677 100644 --- a/src/Composer/IO/NullIO.php +++ b/src/Composer/IO/NullIO.php @@ -62,28 +62,28 @@ class NullIO extends BaseIO /** * {@inheritDoc} */ - public function write($messages, $newline = true) + public function write($messages, $newline = true, $verbosity = self::NORMAL) { } /** * {@inheritDoc} */ - public function writeError($messages, $newline = true) + public function writeError($messages, $newline = true, $verbosity = self::NORMAL) { } /** * {@inheritDoc} */ - public function overwrite($messages, $newline = true, $size = 80) + public function overwrite($messages, $newline = true, $size = 80, $verbosity = self::NORMAL) { } /** * {@inheritDoc} */ - public function overwriteError($messages, $newline = true, $size = 80) + public function overwriteError($messages, $newline = true, $size = 80, $verbosity = self::NORMAL) { } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index f59eac7d3..3172e596f 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -529,10 +529,8 @@ class Installer return max(1, $e->getCode()); } - if ($this->io->isVerbose()) { - $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies"); - $this->io->writeError("Analyzed ".$solver->getRuleSetSize()." rules to resolve dependencies"); - } + $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE); + $this->io->writeError("Analyzed ".$solver->getRuleSetSize()." rules to resolve dependencies", true, IOInterface::VERBOSE); // force dev packages to be updated if we update or install from a (potentially new) lock $operations = $this->processDevPackages($localRepo, $pool, $policy, $repositories, $installedRepo, $lockedRepository, $installFromLock, $withDevReqs, 'force-updates', $operations); @@ -578,10 +576,8 @@ class Installer && (!$operation->getTargetPackage()->getSourceReference() || $operation->getTargetPackage()->getSourceReference() === $operation->getInitialPackage()->getSourceReference()) && (!$operation->getTargetPackage()->getDistReference() || $operation->getTargetPackage()->getDistReference() === $operation->getInitialPackage()->getDistReference()) ) { - if ($this->io->isDebug()) { - $this->io->writeError(' - Skipping update of '. $operation->getTargetPackage()->getPrettyName().' to the same reference-locked version'); - $this->io->writeError(''); - } + $this->io->writeError(' - Skipping update of '. $operation->getTargetPackage()->getPrettyName().' to the same reference-locked version', true, IOInterface::DEBUG); + $this->io->writeError('', true, IOInterface::DEBUG); continue; } diff --git a/src/Composer/Installer/PearInstaller.php b/src/Composer/Installer/PearInstaller.php index 146e68b95..35f7855de 100644 --- a/src/Composer/Installer/PearInstaller.php +++ b/src/Composer/Installer/PearInstaller.php @@ -75,9 +75,7 @@ class PearInstaller extends LibraryInstaller $pearExtractor = new PearPackageExtractor($packageArchive); $pearExtractor->extractTo($this->getInstallPath($package), array('php' => '/', 'script' => '/bin', 'data' => '/data'), $vars); - if ($this->io->isVerbose()) { - $this->io->writeError(' Cleaning up'); - } + $this->io->writeError(' Cleaning up', true, IOInterface::VERBOSE); $this->filesystem->unlink($packageArchive); } diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index bfb0d9427..175a5e05b 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -206,9 +206,7 @@ class PluginManager */ private function addPlugin(PluginInterface $plugin) { - if ($this->io->isDebug()) { - $this->io->writeError('Loading plugin '.get_class($plugin)); - } + $this->io->writeError('Loading plugin '.get_class($plugin), true, IOInterface::DEBUG); $this->plugins[] = $plugin; $plugin->activate($this->composer, $this->io); diff --git a/src/Composer/Repository/ArtifactRepository.php b/src/Composer/Repository/ArtifactRepository.php index 81335ef7e..ece78bb1c 100644 --- a/src/Composer/Repository/ArtifactRepository.php +++ b/src/Composer/Repository/ArtifactRepository.php @@ -67,16 +67,12 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito $package = $this->getComposerInformation($file); if (!$package) { - if ($io->isVerbose()) { - $io->writeError("File {$file->getBasename()} doesn't seem to hold a package"); - } + $io->writeError("File {$file->getBasename()} doesn't seem to hold a package", true, IOInterface::VERBOSE); continue; } - if ($io->isVerbose()) { - $template = 'Found package %s (%s) in file %s'; - $io->writeError(sprintf($template, $package->getName(), $package->getPrettyVersion(), $file->getBasename())); - } + $template = 'Found package %s (%s) in file %s'; + $io->writeError(sprintf($template, $package->getName(), $package->getPrettyVersion(), $file->getBasename()), true, IOInterface::VERBOSE); $this->addPackage($package); } diff --git a/src/Composer/Repository/PearRepository.php b/src/Composer/Repository/PearRepository.php index 1d6d4710a..2c3e64bbe 100644 --- a/src/Composer/Repository/PearRepository.php +++ b/src/Composer/Repository/PearRepository.php @@ -105,9 +105,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn try { $normalizedVersion = $versionParser->normalize($version); } catch (\UnexpectedValueException $e) { - if ($this->io->isVerbose()) { - $this->io->writeError('Could not load '.$packageDefinition->getPackageName().' '.$version.': '.$e->getMessage()); - } + $this->io->writeError('Could not load '.$packageDefinition->getPackageName().' '.$version.': '.$e->getMessage(), true, IOInterface::VERBOSE); continue; } diff --git a/src/Composer/Repository/Vcs/GitBitbucketDriver.php b/src/Composer/Repository/Vcs/GitBitbucketDriver.php index 0f0a57c47..7a2781ecb 100644 --- a/src/Composer/Repository/Vcs/GitBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/GitBitbucketDriver.php @@ -160,9 +160,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface } if (!extension_loaded('openssl')) { - if ($io->isVerbose()) { - $io->writeError('Skipping Bitbucket git driver for '.$url.' because the OpenSSL PHP extension is missing.'); - } + $io->writeError('Skipping Bitbucket git driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE); return false; } diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index cdd9df89f..4a4e1fcea 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -268,9 +268,7 @@ class GitHubDriver extends VcsDriver } if (!extension_loaded('openssl')) { - if ($io->isVerbose()) { - $io->writeError('Skipping GitHub driver for '.$url.' because the OpenSSL PHP extension is missing.'); - } + $io->writeError('Skipping GitHub driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE); return false; } diff --git a/src/Composer/Repository/Vcs/GitLabDriver.php b/src/Composer/Repository/Vcs/GitLabDriver.php index fa13f952c..8642c2d42 100644 --- a/src/Composer/Repository/Vcs/GitLabDriver.php +++ b/src/Composer/Repository/Vcs/GitLabDriver.php @@ -367,9 +367,7 @@ class GitLabDriver extends VcsDriver } if ('https' === $scheme && !extension_loaded('openssl')) { - if ($io->isVerbose()) { - $io->write('Skipping GitLab driver for '.$url.' because the OpenSSL PHP extension is missing.'); - } + $io->writeError('Skipping GitLab driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE); return false; } diff --git a/src/Composer/Repository/Vcs/HgBitbucketDriver.php b/src/Composer/Repository/Vcs/HgBitbucketDriver.php index 3beeee440..eb6808601 100644 --- a/src/Composer/Repository/Vcs/HgBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/HgBitbucketDriver.php @@ -170,9 +170,7 @@ class HgBitbucketDriver extends VcsDriver } if (!extension_loaded('openssl')) { - if ($io->isVerbose()) { - $io->writeError('Skipping Bitbucket hg driver for '.$url.' because the OpenSSL PHP extension is missing.'); - } + $io->writeError('Skipping Bitbucket hg driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE); return false; } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 2d94e5909..f4c5f3150 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -227,9 +227,7 @@ class RemoteFilesystem unset($tempAdditionalOptions); $userlandFollow = isset($options['http']['follow_location']) && !$options['http']['follow_location']; - if ($this->io->isDebug()) { - $this->io->writeError((substr($fileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $fileUrl); - } + $this->io->writeError((substr($fileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $fileUrl, true, IOInterface::DEBUG); if (isset($options['github-token'])) { $fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['github-token']; @@ -609,13 +607,11 @@ class RemoteFilesystem // Handle subjectAltName on lesser PHP's. $certMap = $this->peerCertificateMap[$urlAuthority]; - if ($this->io->isDebug()) { - $this->io->writeError(sprintf( - 'Using %s as CN for subjectAltName enabled host %s', - $certMap['cn'], - $urlAuthority - )); - } + $this->io->writeError(sprintf( + 'Using %s as CN for subjectAltName enabled host %s', + $certMap['cn'], + $urlAuthority + ), true, IOInterface::DEBUG); $tlsOptions['ssl']['CN_match'] = $certMap['cn']; $tlsOptions['ssl']['peer_fingerprint'] = $certMap['fp']; @@ -689,9 +685,7 @@ class RemoteFilesystem if (!empty($targetUrl)) { $this->redirects++; - if ($this->io->isDebug()) { - $this->io->writeError(sprintf('Following redirect (%u) %s', $this->redirects, $targetUrl)); - } + $this->io->writeError(sprintf('Following redirect (%u) %s', $this->redirects, $targetUrl), true, IOInterface::DEBUG); $additionalOptions['redirects'] = $this->redirects; @@ -914,9 +908,7 @@ class RemoteFilesystem return $files[$filename]; } - if ($this->io->isDebug()) { - $this->io->writeError('Checking CA file '.realpath($filename)); - } + $this->io->writeError('Checking CA file '.realpath($filename), true, IOInterface::DEBUG); $contents = file_get_contents($filename); // assume the CA is valid if php is vulnerable to diff --git a/tests/Composer/Test/ApplicationTest.php b/tests/Composer/Test/ApplicationTest.php index 68d17d3f6..e7ab59c78 100644 --- a/tests/Composer/Test/ApplicationTest.php +++ b/tests/Composer/Test/ApplicationTest.php @@ -14,6 +14,7 @@ namespace Composer\Test; use Composer\Console\Application; use Composer\TestCase; +use Symfony\Component\Console\Output\OutputInterface; class ApplicationTest extends TestCase { @@ -30,11 +31,19 @@ class ApplicationTest extends TestCase $index = 0; if (extension_loaded('xdebug')) { + $outputMock->expects($this->at($index++)) + ->method("getVerbosity") + ->willReturn(OutputInterface::VERBOSITY_NORMAL); + $outputMock->expects($this->at($index++)) ->method("write") ->with($this->equalTo('You are running composer with xdebug enabled. This has a major impact on runtime performance. See https://getcomposer.org/xdebug')); } + $outputMock->expects($this->at($index++)) + ->method("getVerbosity") + ->willReturn(OutputInterface::VERBOSITY_NORMAL); + $outputMock->expects($this->at($index++)) ->method("write") ->with($this->equalTo(sprintf('Warning: This development build of composer is over 60 days old. It is recommended to update it by running "%s self-update" to get the latest version.', $_SERVER['PHP_SELF']))); diff --git a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index 2dd9f8a4a..e0c1fa45e 100644 --- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -15,9 +15,11 @@ namespace Composer\Test\EventDispatcher; use Composer\EventDispatcher\Event; use Composer\Installer\InstallerEvents; use Composer\TestCase; +use Composer\IO\BufferIO; use Composer\Script\ScriptEvents; use Composer\Script\CommandEvent; use Composer\Util\ProcessExecutor; +use Symfony\Component\Console\Output\OutputInterface; class EventDispatcherTest extends TestCase { @@ -101,7 +103,7 @@ class EventDispatcherTest extends TestCase $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') ->setConstructorArgs(array( $this->getMock('Composer\Composer'), - $io = $this->getMock('Composer\IO\IOInterface'), + $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE), $process, )) ->setMethods(array( @@ -123,23 +125,12 @@ class EventDispatcherTest extends TestCase ->method('getListeners') ->will($this->returnValue($listeners)); - $io->expects($this->any()) - ->method('isVerbose') - ->willReturn(1); - - $io->expects($this->at(1)) - ->method('writeError') - ->with($this->equalTo('> post-install-cmd: echo -n foo')); - - $io->expects($this->at(3)) - ->method('writeError') - ->with($this->equalTo('> post-install-cmd: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod')); - - $io->expects($this->at(5)) - ->method('writeError') - ->with($this->equalTo('> post-install-cmd: echo -n bar')); - $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false); + + $expected = '> post-install-cmd: echo -n foo'.PHP_EOL. + '> post-install-cmd: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'.PHP_EOL. + '> post-install-cmd: echo -n bar'.PHP_EOL; + $this->assertEquals($expected, $io->getOutput()); } public function testDispatcherCanExecuteComposerScriptGroups() @@ -148,7 +139,7 @@ class EventDispatcherTest extends TestCase $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') ->setConstructorArgs(array( $composer = $this->getMock('Composer\Composer'), - $io = $this->getMock('Composer\IO\IOInterface'), + $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE), $process, )) ->setMethods(array( @@ -174,31 +165,13 @@ class EventDispatcherTest extends TestCase return array(); })); - $io->expects($this->any()) - ->method('isVerbose') - ->willReturn(1); - - $io->expects($this->at(1)) - ->method('writeError') - ->with($this->equalTo('> root: @group')); - - $io->expects($this->at(3)) - ->method('writeError') - ->with($this->equalTo('> group: echo -n foo')); - - $io->expects($this->at(5)) - ->method('writeError') - ->with($this->equalTo('> group: @subgroup')); - - $io->expects($this->at(7)) - ->method('writeError') - ->with($this->equalTo('> subgroup: echo -n baz')); - - $io->expects($this->at(9)) - ->method('writeError') - ->with($this->equalTo('> group: echo -n bar')); - $dispatcher->dispatch('root', new CommandEvent('root', $composer, $io)); + $expected = '> root: @group'.PHP_EOL. + '> group: echo -n foo'.PHP_EOL. + '> group: @subgroup'.PHP_EOL. + '> subgroup: echo -n baz'.PHP_EOL. + '> group: echo -n bar'.PHP_EOL; + $this->assertEquals($expected, $io->getOutput()); } /** diff --git a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test index 26861b1c6..1e0b9ff2c 100644 --- a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test +++ b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test @@ -24,12 +24,12 @@ Abandoned packages are flagged --RUN-- install --EXPECT-OUTPUT-- -Loading composer repositories with package information -Installing dependencies (including require-dev) +Loading composer repositories with package information +Installing dependencies (including require-dev) Package a/a is abandoned, you should avoid using it. No replacement was suggested. Package c/c is abandoned, you should avoid using it. Use b/b instead. -Writing lock file -Generating autoload files +Writing lock file +Generating autoload files --EXPECT-- Installing a/a (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test index d8b09c4a1..e7c6cd984 100644 --- a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test +++ b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test @@ -21,9 +21,9 @@ Broken dependencies should not lead to a replacer being installed which is not m --RUN-- install --EXPECT-OUTPUT-- -Loading composer repositories with package information -Installing dependencies (including require-dev) -Your requirements could not be resolved to an installable set of packages. +Loading composer repositories with package information +Installing dependencies (including require-dev) +Your requirements could not be resolved to an installable set of packages. Problem 1 - c/c 1.0.0 requires x/x 1.0 -> no matching package found. diff --git a/tests/Composer/Test/Fixtures/installer/suggest-installed.test b/tests/Composer/Test/Fixtures/installer/suggest-installed.test index 94f6c2016..4929f972e 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-installed.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-installed.test @@ -19,10 +19,10 @@ Suggestions are not displayed for installed packages --RUN-- install --EXPECT-OUTPUT-- -Loading composer repositories with package information -Installing dependencies (including require-dev) -Writing lock file -Generating autoload files +Loading composer repositories with package information +Installing dependencies (including require-dev) +Writing lock file +Generating autoload files --EXPECT-- Installing a/a (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/suggest-prod.test b/tests/Composer/Test/Fixtures/installer/suggest-prod.test index 290ccf4bb..c89bb0c20 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-prod.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-prod.test @@ -17,10 +17,10 @@ Suggestions are not displayed in non-dev mode --RUN-- install --no-dev --EXPECT-OUTPUT-- -Loading composer repositories with package information -Installing dependencies -Writing lock file -Generating autoload files +Loading composer repositories with package information +Installing dependencies +Writing lock file +Generating autoload files --EXPECT-- Installing a/a (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test index 99d13a720..5d64d2176 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test @@ -19,10 +19,10 @@ Suggestions are not displayed for packages if they are replaced --RUN-- install --EXPECT-OUTPUT-- -Loading composer repositories with package information -Installing dependencies (including require-dev) -Writing lock file -Generating autoload files +Loading composer repositories with package information +Installing dependencies (including require-dev) +Writing lock file +Generating autoload files --EXPECT-- Installing c/c (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test index d7e026e98..d04b6c8d5 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test @@ -17,11 +17,11 @@ Suggestions are displayed --RUN-- install --EXPECT-OUTPUT-- -Loading composer repositories with package information -Installing dependencies (including require-dev) +Loading composer repositories with package information +Installing dependencies (including require-dev) a/a suggests installing b/b (an obscure reason) -Writing lock file -Generating autoload files +Writing lock file +Generating autoload files --EXPECT-- Installing a/a (1.0.0) diff --git a/tests/Composer/Test/IO/ConsoleIOTest.php b/tests/Composer/Test/IO/ConsoleIOTest.php index a300350b9..ca7d420c9 100644 --- a/tests/Composer/Test/IO/ConsoleIOTest.php +++ b/tests/Composer/Test/IO/ConsoleIOTest.php @@ -14,6 +14,7 @@ namespace Composer\Test\IO; use Composer\IO\ConsoleIO; use Composer\TestCase; +use Symfony\Component\Console\Output\OutputInterface; class ConsoleIOTest extends TestCase { @@ -40,6 +41,9 @@ class ConsoleIOTest extends TestCase { $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface'); $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface'); + $outputMock->expects($this->once()) + ->method('getVerbosity') + ->willReturn(OutputInterface::VERBOSITY_NORMAL); $outputMock->expects($this->once()) ->method('write') ->with($this->equalTo('some information about something'), $this->equalTo(false)); @@ -53,6 +57,9 @@ class ConsoleIOTest extends TestCase { $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface'); $outputMock = $this->getMock('Symfony\Component\Console\Output\ConsoleOutputInterface'); + $outputMock->expects($this->once()) + ->method('getVerbosity') + ->willReturn(OutputInterface::VERBOSITY_NORMAL); $outputMock->expects($this->once()) ->method('getErrorOutput') ->willReturn($outputMock); @@ -69,6 +76,9 @@ class ConsoleIOTest extends TestCase { $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface'); $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface'); + $outputMock->expects($this->once()) + ->method('getVerbosity') + ->willReturn(OutputInterface::VERBOSITY_NORMAL); $outputMock->expects($this->once()) ->method('write') ->with( @@ -95,25 +105,28 @@ class ConsoleIOTest extends TestCase $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface'); $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface'); - $outputMock->expects($this->at(0)) + $outputMock->expects($this->any()) + ->method('getVerbosity') + ->willReturn(OutputInterface::VERBOSITY_NORMAL); + $outputMock->expects($this->at(1)) ->method('write') ->with($this->equalTo('something (strlen = 23)')); - $outputMock->expects($this->at(1)) + $outputMock->expects($this->at(3)) ->method('write') ->with($this->equalTo(str_repeat("\x08", 23)), $this->equalTo(false)); - $outputMock->expects($this->at(2)) + $outputMock->expects($this->at(5)) ->method('write') ->with($this->equalTo('shorter (12)'), $this->equalTo(false)); - $outputMock->expects($this->at(3)) + $outputMock->expects($this->at(7)) ->method('write') ->with($this->equalTo(str_repeat(' ', 11)), $this->equalTo(false)); - $outputMock->expects($this->at(4)) + $outputMock->expects($this->at(9)) ->method('write') ->with($this->equalTo(str_repeat("\x08", 11)), $this->equalTo(false)); - $outputMock->expects($this->at(5)) + $outputMock->expects($this->at(11)) ->method('write') ->with($this->equalTo(str_repeat("\x08", 12)), $this->equalTo(false)); - $outputMock->expects($this->at(6)) + $outputMock->expects($this->at(13)) ->method('write') ->with($this->equalTo('something longer than initial (34)')); diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 5339b8ff3..b0a51fea7 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -26,7 +26,10 @@ use Composer\Test\Mock\InstalledFilesystemRepositoryMock; use Composer\Test\Mock\InstallationManagerMock; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\StreamOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Formatter\OutputFormatter; use Composer\TestCase; +use Composer\IO\BufferIO; class InstallerTest extends TestCase { @@ -146,18 +149,7 @@ class InstallerTest extends TestCase } } - $output = null; - $io = $this->getMock('Composer\IO\IOInterface'); - $callback = function ($text, $newline) use (&$output) { - $output .= $text . ($newline ? "\n" : ""); - }; - $io->expects($this->any()) - ->method('write') - ->will($this->returnCallback($callback)); - $io->expects($this->any()) - ->method('writeError') - ->will($this->returnCallback($callback)); - + $io = new BufferIO('', OutputInterface::VERBOSITY_NORMAL, new OutputFormatter(false)); $composer = FactoryMock::create($io, $composerConfig); $jsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); @@ -233,6 +225,7 @@ class InstallerTest extends TestCase $appOutput = fopen('php://memory', 'w+'); $result = $application->run(new StringInput($run), new StreamOutput($appOutput)); fseek($appOutput, 0); + $output = str_replace("\r", '', $io->getOutput()); $this->assertEquals($expectExitCode, $result, $output . stream_get_contents($appOutput)); if ($expectLock) { From 2617ec5d28f90bbb41892588baa4da8c2f627d98 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 29 Jan 2016 12:51:23 +0000 Subject: [PATCH 89/98] Use proper defaults for IO authentications --- src/Composer/IO/BaseIO.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Composer/IO/BaseIO.php b/src/Composer/IO/BaseIO.php index 3a5a1d701..a2ed28874 100644 --- a/src/Composer/IO/BaseIO.php +++ b/src/Composer/IO/BaseIO.php @@ -60,9 +60,9 @@ abstract class BaseIO implements IOInterface */ public function loadConfiguration(Config $config) { - $githubOauth = $config->get('github-oauth'); - $gitlabOauth = $config->get('gitlab-oauth'); - $httpBasic = $config->get('http-basic'); + $githubOauth = $config->get('github-oauth') ?: array(); + $gitlabOauth = $config->get('gitlab-oauth') ?: array(); + $httpBasic = $config->get('http-basic') ?: array(); // Use COMPOSER_AUTH environment variable if set if ($composerAuthEnv = getenv('COMPOSER_AUTH')) { From 7c3e621102106fd28af0128fdfd750af926fc569 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 29 Jan 2016 12:58:17 +0000 Subject: [PATCH 90/98] Make sure COMPOSER_AUTH is also loaded in Config, refs #4546 --- src/Composer/Factory.php | 14 +++++++++++++ src/Composer/IO/BaseIO.php | 41 ++++++++------------------------------ 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 989975fde..a7610242f 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -190,6 +190,20 @@ class Factory } $config->setAuthConfigSource(new JsonConfigSource($file, true)); + // load COMPOSER_AUTH environment variable if set + if ($composerAuthEnv = getenv('COMPOSER_AUTH')) { + $authData = json_decode($composerAuthEnv, true); + + if (is_null($authData)) { + throw new \UnexpectedValueException('COMPOSER_AUTH environment variable is malformed, should be a valid JSON object'); + } + + if ($io && $io->isDebug()) { + $io->writeError('Loading auth config from COMPOESR_AUTH'); + } + $config->merge(array('config' => $authData)); + } + return $config; } diff --git a/src/Composer/IO/BaseIO.php b/src/Composer/IO/BaseIO.php index a2ed28874..ad7e32df4 100644 --- a/src/Composer/IO/BaseIO.php +++ b/src/Composer/IO/BaseIO.php @@ -64,46 +64,21 @@ abstract class BaseIO implements IOInterface $gitlabOauth = $config->get('gitlab-oauth') ?: array(); $httpBasic = $config->get('http-basic') ?: array(); - // Use COMPOSER_AUTH environment variable if set - if ($composerAuthEnv = getenv('COMPOSER_AUTH')) { - $authData = json_decode($composerAuthEnv, true); - - if (is_null($authData)) { - throw new \UnexpectedValueException('COMPOSER_AUTH environment variable is malformed'); - } - - if (isset($authData['github-oauth'])) { - $githubOauth = array_merge($githubOauth, $authData['github-oauth']); - } - if (isset($authData['gitlab-oauth'])) { - $gitlabOauth = array_merge($gitlabOauth, $authData['gitlab-oauth']); - } - if (isset($authData['http-basic'])) { - $httpBasic = array_merge($httpBasic, $authData['http-basic']); - } - } - // reload oauth token from config if available - if ($githubOauth) { - foreach ($githubOauth as $domain => $token) { - if (!preg_match('{^[a-z0-9]+$}', $token)) { - throw new \UnexpectedValueException('Your github oauth token for '.$domain.' contains invalid characters: "'.$token.'"'); - } - $this->setAuthentication($domain, $token, 'x-oauth-basic'); + foreach ($githubOauth as $domain => $token) { + if (!preg_match('{^[a-z0-9]+$}', $token)) { + throw new \UnexpectedValueException('Your github oauth token for '.$domain.' contains invalid characters: "'.$token.'"'); } + $this->setAuthentication($domain, $token, 'x-oauth-basic'); } - if ($gitlabOauth) { - foreach ($gitlabOauth as $domain => $token) { - $this->setAuthentication($domain, $token, 'oauth2'); - } + foreach ($gitlabOauth as $domain => $token) { + $this->setAuthentication($domain, $token, 'oauth2'); } // reload http basic credentials from config if available - if ($httpBasic) { - foreach ($httpBasic as $domain => $cred) { - $this->setAuthentication($domain, $cred['username'], $cred['password']); - } + foreach ($httpBasic as $domain => $cred) { + $this->setAuthentication($domain, $cred['username'], $cred['password']); } // setup process timeout From cc75946ef23ffcc8f14122dc62306d747fd3ccd5 Mon Sep 17 00:00:00 2001 From: Bilal Amarni Date: Fri, 29 Jan 2016 17:13:44 +0100 Subject: [PATCH 91/98] typos --- CHANGELOG.md | 2 +- src/Composer/Factory.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65e7018f6..6700114fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ * Added --strict to the `validate` command to treat any warning as an error that then returns a non-zero exit code * Added a dependency on composer/semver, which is the externalized lib for all the version constraints parsing and handling * Added support for classmap autoloading to load plugin classes and script handlers - * Added `bin-compat` config option that if set to `full` will create .bat proxy for binaries even if Compoesr runs in a linux VM + * Added `bin-compat` config option that if set to `full` will create .bat proxy for binaries even if Composer runs in a linux VM * Added SPDX 2.0 support, and externalized that in a composer/spdx-licenses lib * Added warnings when the classmap autoloader finds duplicate classes * Added --file to the `archive` command to choose the filename diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index a7610242f..058709275 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -199,7 +199,7 @@ class Factory } if ($io && $io->isDebug()) { - $io->writeError('Loading auth config from COMPOESR_AUTH'); + $io->writeError('Loading auth config from COMPOSER_AUTH'); } $config->merge(array('config' => $authData)); } From 47aa87ea971cdbcac06e4bb9dce4ae60aa7779de Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Tue, 2 Feb 2016 10:26:43 +0100 Subject: [PATCH 92/98] use full json content to determine reference, closes #4859 --- src/Composer/Repository/PathRepository.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Composer/Repository/PathRepository.php b/src/Composer/Repository/PathRepository.php index c3266543b..7fa004eb5 100644 --- a/src/Composer/Repository/PathRepository.php +++ b/src/Composer/Repository/PathRepository.php @@ -125,17 +125,16 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn $package['dist'] = array( 'type' => 'path', 'url' => $url, - 'reference' => '', + 'reference' => sha1($json), ); if (!isset($package['version'])) { $package['version'] = $this->versionGuesser->guessVersion($package, $path) ?: 'dev-master'; } + $output = ''; if (is_dir($path . DIRECTORY_SEPARATOR . '.git') && 0 === $this->process->execute('git log -n1 --pretty=%H', $output, $path)) { $package['dist']['reference'] = trim($output); - } else { - $package['dist']['reference'] = Locker::getContentHash($json); } $package = $this->loader->load($package); From 8b4761ff14a74871df8f6d5739158e68f0b5562e Mon Sep 17 00:00:00 2001 From: Bilal Amarni Date: Wed, 3 Feb 2016 15:46:15 +0100 Subject: [PATCH 93/98] [doc] add -H flag to sudo commands --- doc/00-intro.md | 2 +- doc/03-cli.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/00-intro.md b/doc/00-intro.md index 1d09f2339..872bdff2c 100644 --- a/doc/00-intro.md +++ b/doc/00-intro.md @@ -109,7 +109,7 @@ mv composer.phar /usr/local/bin/composer A quick copy-paste version including sudo: ```sh -curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer +curl -sS https://getcomposer.org/installer | sudo -H php -- --install-dir=/usr/local/bin --filename=composer ``` > **Note:** On some versions of OSX the `/usr` directory does not exist by diff --git a/doc/03-cli.md b/doc/03-cli.md index 94a7a963c..b35d8f805 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -413,7 +413,7 @@ If you have installed Composer for your entire system (see [global installation] you may have to run the command with `root` privileges ```sh -sudo composer self-update +sudo -H composer self-update ``` ### Options From d93f7b8a10a01ecd608a4cb324c6451245279fb9 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 3 Feb 2016 14:57:32 +0000 Subject: [PATCH 94/98] Remove warnings for non-writable dirs, refs #3588 --- src/Composer/Command/SelfUpdateCommand.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index fff159158..16b98020a 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -88,9 +88,6 @@ EOT if (!is_writable($tmpDir)) { throw new FilesystemException('Composer update failed: the "'.$tmpDir.'" directory used to download the temp file could not be written'); } - if (!is_writable($localFilename)) { - throw new FilesystemException('Composer update failed: the "'.$localFilename.'" file could not be written'); - } if ($input->getOption('rollback')) { return $this->rollback($output, $rollbackDir, $localFilename); @@ -271,10 +268,6 @@ TAGSPUBKEY throw new \UnexpectedValueException('Composer rollback failed: no installation to roll back to in "'.$rollbackDir.'"'); } - if (!is_writable($rollbackDir)) { - throw new FilesystemException('Composer rollback failed: the "'.$rollbackDir.'" dir could not be written to'); - } - $old = $rollbackDir . '/' . $rollbackVersion . self::OLD_INSTALL_EXT; if (!is_file($old)) { From f2a2b1836754de7f33a99f7fd73a472f4717106f Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Wed, 3 Feb 2016 22:25:43 +0100 Subject: [PATCH 95/98] Added Platform utility and unit test for it. --- src/Composer/Util/Platform.php | 28 ++++++++++++++++++++++ tests/Composer/Test/Util/PlatformTest.php | 29 +++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/Composer/Util/Platform.php create mode 100644 tests/Composer/Test/Util/PlatformTest.php diff --git a/src/Composer/Util/Platform.php b/src/Composer/Util/Platform.php new file mode 100644 index 000000000..eafb88b7a --- /dev/null +++ b/src/Composer/Util/Platform.php @@ -0,0 +1,28 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +/** + * Platform helper for uniform platform-specific tests. + * + * @author Niels Keurentjes + */ +class Platform +{ + /** + * @return bool Whether the host machine is running a Windows OS + */ + public static function isWindows() + { + return defined('PHP_WINDOWS_VERSION_BUILD'); + } +} diff --git a/tests/Composer/Test/Util/PlatformTest.php b/tests/Composer/Test/Util/PlatformTest.php new file mode 100644 index 000000000..3d82fb96f --- /dev/null +++ b/tests/Composer/Test/Util/PlatformTest.php @@ -0,0 +1,29 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Util; + +use Composer\Util\Platform; + +/** + * PlatformTest + * + * @author Niels Keurentjes + */ +class PlatformTest extends \PHPUnit_Framework_TestCase +{ + public function testWindows() + { + // Compare 2 common tests for Windows to the built-in Windows test + $this->assertEquals(('\\' === DIRECTORY_SEPARATOR), Platform::isWindows()); + $this->assertEquals(defined('PHP_WINDOWS_VERSION_MAJOR'), Platform::isWindows()); + } +} From 0dab63e0507f548149c85a2371b044b628cf845c Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Wed, 3 Feb 2016 22:39:16 +0100 Subject: [PATCH 96/98] Unified all Windows tests throughout the code. --- src/Composer/Command/ConfigCommand.php | 5 +++-- src/Composer/Command/HomeCommand.php | 3 ++- src/Composer/Command/ShowCommand.php | 3 ++- src/Composer/Console/Application.php | 3 ++- src/Composer/Downloader/GitDownloader.php | 5 +++-- src/Composer/Downloader/GzipDownloader.php | 3 ++- src/Composer/Downloader/RarDownloader.php | 5 +++-- src/Composer/Downloader/ZipDownloader.php | 5 +++-- src/Composer/Factory.php | 7 ++++--- src/Composer/Installer/LibraryInstaller.php | 3 ++- src/Composer/Installer/PearInstaller.php | 3 ++- src/Composer/Util/Filesystem.php | 16 ++++++++-------- src/Composer/Util/Perforce.php | 5 +---- src/Composer/Util/ProcessExecutor.php | 2 +- src/Composer/Util/TlsHelper.php | 2 +- .../Test/Downloader/GitDownloaderTest.php | 3 ++- .../Test/Downloader/HgDownloaderTest.php | 7 ++----- .../Test/Downloader/XzDownloaderTest.php | 3 ++- .../Test/Repository/Vcs/SvnDriverTest.php | 3 ++- tests/Composer/Test/Util/SvnTest.php | 7 ++----- 20 files changed, 49 insertions(+), 44 deletions(-) diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index 94fccc2e5..722bc94cc 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -12,6 +12,7 @@ namespace Composer\Command; +use Composer\Util\Platform; use Composer\Util\Silencer; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; @@ -184,7 +185,7 @@ EOT if ($input->getOption('editor')) { $editor = escapeshellcmd(getenv('EDITOR')); if (!$editor) { - if (defined('PHP_WINDOWS_VERSION_BUILD')) { + if (Platform::isWindows()) { $editor = 'notepad'; } else { foreach (array('vim', 'vi', 'nano', 'pico', 'ed') as $candidate) { @@ -197,7 +198,7 @@ EOT } $file = $input->getOption('auth') ? $this->authConfigFile->getPath() : $this->configFile->getPath(); - system($editor . ' ' . $file . (defined('PHP_WINDOWS_VERSION_BUILD') ? '' : ' > `tty`')); + system($editor . ' ' . $file . (Platform::isWindows() ? '' : ' > `tty`')); return 0; } diff --git a/src/Composer/Command/HomeCommand.php b/src/Composer/Command/HomeCommand.php index fff1f86eb..150b8c71b 100644 --- a/src/Composer/Command/HomeCommand.php +++ b/src/Composer/Command/HomeCommand.php @@ -16,6 +16,7 @@ use Composer\Factory; use Composer\Package\CompletePackageInterface; use Composer\Repository\RepositoryInterface; use Composer\Repository\ArrayRepository; +use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -117,7 +118,7 @@ EOT { $url = ProcessExecutor::escape($url); - if (defined('PHP_WINDOWS_VERSION_MAJOR')) { + if (Platform::isWindows()) { return passthru('start "web" explorer "' . $url . '"'); } diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index e3e1ffd71..5ddce2954 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -20,6 +20,7 @@ use Composer\Semver\VersionParser; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Package\PackageInterface; +use Composer\Util\Platform; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; @@ -232,7 +233,7 @@ EOT // outside of a real terminal, use space without a limit $width = PHP_INT_MAX; } - if (defined('PHP_WINDOWS_VERSION_BUILD')) { + if (Platform::isWindows()) { $width--; } diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index f77876100..07d517eae 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -12,6 +12,7 @@ namespace Composer\Console; +use Composer\Util\Platform; use Composer\Util\Silencer; use Symfony\Component\Console\Application as BaseApplication; use Symfony\Component\Console\Input\InputInterface; @@ -219,7 +220,7 @@ class Application extends BaseApplication } Silencer::restore(); - if (defined('PHP_WINDOWS_VERSION_BUILD') && false !== strpos($exception->getMessage(), 'The system cannot find the path specified')) { + if (Platform::isWindows() && false !== strpos($exception->getMessage(), 'The system cannot find the path specified')) { $io->writeError('The following exception may be caused by a stale entry in your cmd.exe AutoRun', true, IOInterface::QUIET); $io->writeError('Check https://getcomposer.org/doc/articles/troubleshooting.md#-the-system-cannot-find-the-path-specified-windows- for details', true, IOInterface::QUIET); } diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 7db561889..48e0d4b39 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -14,6 +14,7 @@ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Composer\Util\Git as GitUtil; +use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\IO\IOInterface; use Composer\Util\Filesystem; @@ -43,7 +44,7 @@ class GitDownloader extends VcsDownloader $path = $this->normalizePath($path); $ref = $package->getSourceReference(); - $flag = defined('PHP_WINDOWS_VERSION_MAJOR') ? '/D ' : ''; + $flag = Platform::isWindows() ? '/D ' : ''; $command = 'git clone --no-checkout %s %s && cd '.$flag.'%2$s && git remote add composer %1$s && git fetch composer'; $this->io->writeError(" Cloning ".$ref); @@ -353,7 +354,7 @@ class GitDownloader extends VcsDownloader protected function normalizePath($path) { - if (defined('PHP_WINDOWS_VERSION_MAJOR') && strlen($path) > 0) { + if (Platform::isWindows() && strlen($path) > 0) { $basePath = $path; $removed = array(); diff --git a/src/Composer/Downloader/GzipDownloader.php b/src/Composer/Downloader/GzipDownloader.php index ae7d7f17a..819af79f0 100644 --- a/src/Composer/Downloader/GzipDownloader.php +++ b/src/Composer/Downloader/GzipDownloader.php @@ -16,6 +16,7 @@ use Composer\Config; use Composer\Cache; use Composer\EventDispatcher\EventDispatcher; use Composer\Package\PackageInterface; +use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Util\RemoteFilesystem; use Composer\IO\IOInterface; @@ -40,7 +41,7 @@ class GzipDownloader extends ArchiveDownloader $targetFilepath = $path . DIRECTORY_SEPARATOR . basename(substr($file, 0, -3)); // Try to use gunzip on *nix - if (!defined('PHP_WINDOWS_VERSION_BUILD')) { + if (!Platform::isWindows()) { $command = 'gzip -cd ' . ProcessExecutor::escape($file) . ' > ' . ProcessExecutor::escape($targetFilepath); if (0 === $this->process->execute($command, $ignoredOutput)) { diff --git a/src/Composer/Downloader/RarDownloader.php b/src/Composer/Downloader/RarDownloader.php index 81e11785e..2a0c98cf9 100644 --- a/src/Composer/Downloader/RarDownloader.php +++ b/src/Composer/Downloader/RarDownloader.php @@ -15,6 +15,7 @@ namespace Composer\Downloader; use Composer\Config; use Composer\Cache; use Composer\EventDispatcher\EventDispatcher; +use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Util\RemoteFilesystem; use Composer\IO\IOInterface; @@ -42,7 +43,7 @@ class RarDownloader extends ArchiveDownloader $processError = null; // Try to use unrar on *nix - if (!defined('PHP_WINDOWS_VERSION_BUILD')) { + if (!Platform::isWindows()) { $command = 'unrar x ' . ProcessExecutor::escape($file) . ' ' . ProcessExecutor::escape($path) . ' && chmod -R u+w ' . ProcessExecutor::escape($path); if (0 === $this->process->execute($command, $ignoredOutput)) { @@ -65,7 +66,7 @@ class RarDownloader extends ArchiveDownloader $error = "Could not decompress the archive, enable the PHP rar extension or install unrar.\n" . $iniMessage . "\n" . $processError; - if (!defined('PHP_WINDOWS_VERSION_BUILD')) { + if (!Platform::isWindows()) { $error = "Could not decompress the archive, enable the PHP rar extension.\n" . $iniMessage; } diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index 6faaaaa4f..5f483975c 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -15,6 +15,7 @@ namespace Composer\Downloader; use Composer\Config; use Composer\Cache; use Composer\EventDispatcher\EventDispatcher; +use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Util\RemoteFilesystem; use Composer\IO\IOInterface; @@ -38,7 +39,7 @@ class ZipDownloader extends ArchiveDownloader $processError = null; // try to use unzip on *nix - if (!defined('PHP_WINDOWS_VERSION_BUILD')) { + if (!Platform::isWindows()) { $command = 'unzip '.ProcessExecutor::escape($file).' -d '.ProcessExecutor::escape($path) . ' && chmod -R u+w ' . ProcessExecutor::escape($path); try { if (0 === $this->process->execute($command, $ignoredOutput)) { @@ -64,7 +65,7 @@ class ZipDownloader extends ArchiveDownloader $error = "Could not decompress the archive, enable the PHP zip extension or install unzip.\n" . $iniMessage . "\n" . $processError; - if (!defined('PHP_WINDOWS_VERSION_BUILD')) { + if (!Platform::isWindows()) { $error = "Could not decompress the archive, enable the PHP zip extension.\n" . $iniMessage; } diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 058709275..2f3491fd4 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -20,6 +20,7 @@ use Composer\Package\Version\VersionGuesser; use Composer\Repository\RepositoryManager; use Composer\Repository\WritableRepositoryInterface; use Composer\Util\Filesystem; +use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Util\RemoteFilesystem; use Composer\Util\Silencer; @@ -51,7 +52,7 @@ class Factory return $home; } - if (defined('PHP_WINDOWS_VERSION_MAJOR')) { + if (Platform::isWindows()) { if (!getenv('APPDATA')) { throw new \RuntimeException('The APPDATA or COMPOSER_HOME environment variable must be set for composer to run correctly'); } @@ -90,7 +91,7 @@ class Factory return $homeEnv . '/cache'; } - if (defined('PHP_WINDOWS_VERSION_MAJOR')) { + if (Platform::isWindows()) { if ($cacheDir = getenv('LOCALAPPDATA')) { $cacheDir .= '/Composer'; } else { @@ -125,7 +126,7 @@ class Factory return $homeEnv; } - if (defined('PHP_WINDOWS_VERSION_MAJOR')) { + if (Platform::isWindows()) { return strtr($home, '\\', '/'); } diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index b14659f7b..31090de00 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -17,6 +17,7 @@ use Composer\IO\IOInterface; use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; use Composer\Util\Filesystem; +use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Util\Silencer; @@ -241,7 +242,7 @@ class LibraryInstaller implements InstallerInterface } if ($this->binCompat === "auto") { - if (defined('PHP_WINDOWS_VERSION_BUILD')) { + if (Platform::isWindows()) { $this->installFullBinaries($binPath, $link, $bin, $package); } else { $this->installSymlinkBinaries($binPath, $link); diff --git a/src/Composer/Installer/PearInstaller.php b/src/Composer/Installer/PearInstaller.php index 35f7855de..0e16fcb32 100644 --- a/src/Composer/Installer/PearInstaller.php +++ b/src/Composer/Installer/PearInstaller.php @@ -17,6 +17,7 @@ use Composer\Composer; use Composer\Downloader\PearPackageExtractor; use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; +use Composer\Util\Platform; use Composer\Util\ProcessExecutor; /** @@ -53,7 +54,7 @@ class PearInstaller extends LibraryInstaller parent::installCode($package); parent::initializeBinDir(); - $isWindows = defined('PHP_WINDOWS_VERSION_BUILD'); + $isWindows = Platform::isWindows(); $php_bin = $this->binDir . ($isWindows ? '/composer-php.bat' : '/composer-php'); if (!$isWindows) { diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index f7d812de8..e29a60673 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -110,7 +110,7 @@ class Filesystem return $this->removeDirectoryPhp($directory); } - if (defined('PHP_WINDOWS_VERSION_BUILD')) { + if (Platform::isWindows()) { $cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape(realpath($directory))); } else { $cmd = sprintf('rm -rf %s', ProcessExecutor::escape($directory)); @@ -181,10 +181,10 @@ class Filesystem { if (!@$this->unlinkImplementation($path)) { // retry after a bit on windows since it tends to be touchy with mass removals - if (!defined('PHP_WINDOWS_VERSION_BUILD') || (usleep(350000) && !@$this->unlinkImplementation($path))) { + if (!Platform::isWindows() || (usleep(350000) && !@$this->unlinkImplementation($path))) { $error = error_get_last(); $message = 'Could not delete '.$path.': ' . @$error['message']; - if (defined('PHP_WINDOWS_VERSION_BUILD')) { + if (Platform::isWindows()) { $message .= "\nThis can be due to an antivirus or the Windows Search Indexer locking the file while they are analyzed"; } @@ -206,10 +206,10 @@ class Filesystem { if (!@rmdir($path)) { // retry after a bit on windows since it tends to be touchy with mass removals - if (!defined('PHP_WINDOWS_VERSION_BUILD') || (usleep(350000) && !@rmdir($path))) { + if (!Platform::isWindows() || (usleep(350000) && !@rmdir($path))) { $error = error_get_last(); $message = 'Could not delete '.$path.': ' . @$error['message']; - if (defined('PHP_WINDOWS_VERSION_BUILD')) { + if (Platform::isWindows()) { $message .= "\nThis can be due to an antivirus or the Windows Search Indexer locking the file while they are analyzed"; } @@ -264,7 +264,7 @@ class Filesystem return $this->copyThenRemove($source, $target); } - if (defined('PHP_WINDOWS_VERSION_BUILD')) { + if (Platform::isWindows()) { // Try to copy & delete - this is a workaround for random "Access denied" errors. $command = sprintf('xcopy %s %s /E /I /Q /Y', ProcessExecutor::escape($source), ProcessExecutor::escape($target)); $result = $this->processExecutor->execute($command, $output); @@ -460,7 +460,7 @@ class Filesystem public static function getPlatformPath($path) { - if (defined('PHP_WINDOWS_VERSION_BUILD')) { + if (Platform::isWindows()) { $path = preg_replace('{^(?:file:///([a-z])/)}i', 'file://$1:/', $path); } @@ -498,7 +498,7 @@ class Filesystem */ private function unlinkImplementation($path) { - if (defined('PHP_WINDOWS_VERSION_BUILD') && is_dir($path) && is_link($path)) { + if (Platform::isWindows() && is_dir($path) && is_link($path)) { return rmdir($path); } diff --git a/src/Composer/Util/Perforce.php b/src/Composer/Util/Perforce.php index c1eaeebe9..b57a64a1f 100644 --- a/src/Composer/Util/Perforce.php +++ b/src/Composer/Util/Perforce.php @@ -51,10 +51,7 @@ class Perforce public static function create($repoConfig, $port, $path, ProcessExecutor $process, IOInterface $io) { - $isWindows = defined('PHP_WINDOWS_VERSION_BUILD'); - $perforce = new Perforce($repoConfig, $port, $path, $process, $isWindows, $io); - - return $perforce; + return new Perforce($repoConfig, $port, $path, $process, Platform::isWindows(), $io); } public static function checkServerExists($url, ProcessExecutor $processExecutor) diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index f9499f09f..6b778e5eb 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -50,7 +50,7 @@ class ProcessExecutor // make sure that null translate to the proper directory in case the dir is a symlink // and we call a git command, because msysgit does not handle symlinks properly - if (null === $cwd && defined('PHP_WINDOWS_VERSION_BUILD') && false !== strpos($command, 'git') && getcwd()) { + if (null === $cwd && Platform::isWindows() && false !== strpos($command, 'git') && getcwd()) { $cwd = realpath(getcwd()); } diff --git a/src/Composer/Util/TlsHelper.php b/src/Composer/Util/TlsHelper.php index 6ea5cf591..721e93825 100644 --- a/src/Composer/Util/TlsHelper.php +++ b/src/Composer/Util/TlsHelper.php @@ -175,7 +175,7 @@ final class TlsHelper return self::$useOpensslParse = true; } - if ('\\' === DIRECTORY_SEPARATOR) { + if (Platform::isWindows()) { // Windows is probably insecure in this case. return self::$useOpensslParse = false; } diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 26437ada5..9c851fe9a 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -16,6 +16,7 @@ use Composer\Downloader\GitDownloader; use Composer\Config; use Composer\TestCase; use Composer\Util\Filesystem; +use Composer\Util\Platform; class GitDownloaderTest extends TestCase { @@ -353,7 +354,7 @@ class GitDownloaderTest extends TestCase private function winCompat($cmd) { - if (defined('PHP_WINDOWS_VERSION_BUILD')) { + if (Platform::isWindows()) { $cmd = str_replace('cd ', 'cd /D ', $cmd); $cmd = str_replace('composerPath', getcwd().'/composerPath', $cmd); diff --git a/tests/Composer/Test/Downloader/HgDownloaderTest.php b/tests/Composer/Test/Downloader/HgDownloaderTest.php index 75dfa84aa..6b660e383 100644 --- a/tests/Composer/Test/Downloader/HgDownloaderTest.php +++ b/tests/Composer/Test/Downloader/HgDownloaderTest.php @@ -15,6 +15,7 @@ namespace Composer\Test\Downloader; use Composer\Downloader\HgDownloader; use Composer\TestCase; use Composer\Util\Filesystem; +use Composer\Util\Platform; class HgDownloaderTest extends TestCase { @@ -156,10 +157,6 @@ class HgDownloaderTest extends TestCase private function getCmd($cmd) { - if (defined('PHP_WINDOWS_VERSION_BUILD')) { - return strtr($cmd, "'", '"'); - } - - return $cmd; + return Platform::isWindows() ? strtr($cmd, "'", '"') : $cmd; } } diff --git a/tests/Composer/Test/Downloader/XzDownloaderTest.php b/tests/Composer/Test/Downloader/XzDownloaderTest.php index 418776d75..d8e77a2cb 100644 --- a/tests/Composer/Test/Downloader/XzDownloaderTest.php +++ b/tests/Composer/Test/Downloader/XzDownloaderTest.php @@ -15,6 +15,7 @@ namespace Composer\Test\Downloader; use Composer\Downloader\XzDownloader; use Composer\TestCase; use Composer\Util\Filesystem; +use Composer\Util\Platform; use Composer\Util\RemoteFilesystem; class XzDownloaderTest extends TestCase @@ -31,7 +32,7 @@ class XzDownloaderTest extends TestCase public function setUp() { - if (defined('PHP_WINDOWS_VERSION_BUILD')) { + if (Platform::isWindows()) { $this->markTestSkipped('Skip test on Windows'); } $this->testDir = $this->getUniqueTmpDirectory(); diff --git a/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php b/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php index c2ae497ca..881b86ea2 100644 --- a/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php @@ -16,6 +16,7 @@ use Composer\Repository\Vcs\SvnDriver; use Composer\Config; use Composer\TestCase; use Composer\Util\Filesystem; +use Composer\Util\Platform; class SvnDriverTest extends TestCase { @@ -71,7 +72,7 @@ class SvnDriverTest extends TestCase private function getCmd($cmd) { - if (defined('PHP_WINDOWS_VERSION_BUILD')) { + if (Platform::isWindows()) { return strtr($cmd, "'", '"'); } diff --git a/tests/Composer/Test/Util/SvnTest.php b/tests/Composer/Test/Util/SvnTest.php index 55a116376..c16b0e6ce 100644 --- a/tests/Composer/Test/Util/SvnTest.php +++ b/tests/Composer/Test/Util/SvnTest.php @@ -14,6 +14,7 @@ namespace Composer\Test\Util; use Composer\Config; use Composer\IO\NullIO; +use Composer\Util\Platform; use Composer\Util\Svn; class SvnTest extends \PHPUnit_Framework_TestCase @@ -131,10 +132,6 @@ class SvnTest extends \PHPUnit_Framework_TestCase private function getCmd($cmd) { - if (defined('PHP_WINDOWS_VERSION_BUILD')) { - return strtr($cmd, "'", '"'); - } - - return $cmd; + return Platform::isWindows() ? strtr($cmd, "'", '"') : $cmd; } } From 8debdf8764a629e32180e77e0bef59ed70ff2d34 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 4 Feb 2016 00:34:01 +0000 Subject: [PATCH 97/98] Zip extension does not provide zlib support --- composer.json | 3 ++- composer.lock | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 8fcd1ea93..f45775853 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,8 @@ } }, "suggest": { - "ext-zip": "Enabling the zip extension allows you to unzip archives, and allows gzip compression of all internet traffic", + "ext-zip": "Enabling the zip extension allows you to unzip archives", + "ext-zlib": "Allow gzip compression of HTTP requests", "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages" }, "autoload": { diff --git a/composer.lock b/composer.lock index caeba6397..31543e65c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "fdf4b487fa59607376721ebec4ff4783", + "hash": "31b3c13c89f8d6c810637ca1fe8fc6ae", "content-hash": "454148e20b837d9755dee7862f9c7a5d", "packages": [ { From e4877473cf4d47d22952418452b5798276bd1558 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 4 Feb 2016 00:39:31 +0000 Subject: [PATCH 98/98] Fallback to zlib extension to unpack gzip on non Windows systems --- src/Composer/Downloader/GzipDownloader.php | 26 ++++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Composer/Downloader/GzipDownloader.php b/src/Composer/Downloader/GzipDownloader.php index ae7d7f17a..11f2f7f33 100644 --- a/src/Composer/Downloader/GzipDownloader.php +++ b/src/Composer/Downloader/GzipDownloader.php @@ -47,18 +47,19 @@ class GzipDownloader extends ArchiveDownloader return; } + if (extension_loaded('zlib')) { + // Fallback to using the PHP extension. + $this->extractUsingExt($file, $targetFilepath); + + return; + } + $processError = 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput(); throw new \RuntimeException($processError); } // Windows version of PHP has built-in support of gzip functions - $archiveFile = gzopen($file, 'rb'); - $targetFile = fopen($targetFilepath, 'wb'); - while ($string = gzread($archiveFile, 4096)) { - fwrite($targetFile, $string, strlen($string)); - } - gzclose($archiveFile); - fclose($targetFile); + $this->extractUsingExt($file, $targetFilepath); } /** @@ -68,4 +69,15 @@ class GzipDownloader extends ArchiveDownloader { return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME); } + + private function extractUsingExt($file, $targetFilepath) + { + $archiveFile = gzopen($file, 'rb'); + $targetFile = fopen($targetFilepath, 'wb'); + while ($string = gzread($archiveFile, 4096)) { + fwrite($targetFile, $string, strlen($string)); + } + gzclose($archiveFile); + fclose($targetFile); + } }