From d291d65faf7bbc7bd0cb5c50388c15cd11cba4c0 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sun, 18 Mar 2012 21:01:59 +0100 Subject: [PATCH 1/5] Add getIO proxy to base Command class --- src/Composer/Command/Command.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Composer/Command/Command.php b/src/Composer/Command/Command.php index 8744a0da1..98298d139 100644 --- a/src/Composer/Command/Command.php +++ b/src/Composer/Command/Command.php @@ -29,4 +29,12 @@ abstract class Command extends BaseCommand { return $this->getApplication()->getComposer($required); } + + /** + * @return \Composer\IO\ConsoleIO + */ + protected function getIO() + { + return $this->getApplication()->getIO(); + } } From 7f65dd74091459b188c50f53227c1cc857b1180c Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sun, 18 Mar 2012 21:04:15 +0100 Subject: [PATCH 2/5] Use RemoteFilesystem everywhere we do http requests --- src/Composer/Command/CreateProjectCommand.php | 3 ++- src/Composer/Command/SelfUpdateCommand.php | 9 +++---- src/Composer/Command/ValidateCommand.php | 3 ++- src/Composer/Factory.php | 5 ++-- src/Composer/Json/JsonFile.php | 27 ++++++++++++------- .../Repository/ComposerRepository.php | 8 ++++-- src/Composer/Repository/PearRepository.php | 13 +++++---- 7 files changed, 40 insertions(+), 28 deletions(-) diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 4ad2e81d2..88da8adc8 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -23,6 +23,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Json\JsonFile; +use Composer\Util\RemoteFilesystem; /** * Install a package as new project into new directory. @@ -86,7 +87,7 @@ EOT if (null === $repositoryUrl) { $sourceRepo = new ComposerRepository(array('url' => 'http://packagist.org')); } elseif (".json" === substr($repositoryUrl, -5)) { - $sourceRepo = new FilesystemRepository(new JsonFile($repositoryUrl)); + $sourceRepo = new FilesystemRepository(new JsonFile($repositoryUrl, new RemoteFilesystem($io))); } elseif (0 === strpos($repositoryUrl, 'http')) { $sourceRepo = new ComposerRepository(array('url' => $repositoryUrl)); } else { diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 426fc6882..721e44461 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -13,7 +13,7 @@ namespace Composer\Command; use Composer\Composer; -use Composer\Util\StreamContextFactory; +use Composer\Util\RemoteFilesystem; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -40,9 +40,8 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { - $ctx = StreamContextFactory::getContext(); - - $latest = trim(file_get_contents('http://getcomposer.org/version', false, $ctx)); + $rfs = new RemoteFilesystem($this->getIO()); + $latest = trim($rfs->getContents('getcomposer.org', 'http://getcomposer.org/version', false)); if (Composer::VERSION !== $latest) { $output->writeln(sprintf("Updating to version %s.", $latest)); @@ -50,7 +49,7 @@ EOT $remoteFilename = 'http://getcomposer.org/composer.phar'; $localFilename = $_SERVER['argv'][0]; - copy($remoteFilename, $localFilename, $ctx); + $rfs->copy('getcomposer.org', $remoteFilename, $localFilename); } else { $output->writeln("You are using the latest composer version."); } diff --git a/src/Composer/Command/ValidateCommand.php b/src/Composer/Command/ValidateCommand.php index 878a058aa..b65841d71 100644 --- a/src/Composer/Command/ValidateCommand.php +++ b/src/Composer/Command/ValidateCommand.php @@ -17,6 +17,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; use Composer\Json\JsonFile; use Composer\Json\JsonValidationException; +use Composer\Util\RemoteFilesystem; /** * @author Robert Schönthal @@ -55,7 +56,7 @@ EOT $laxValid = false; try { - $json = new JsonFile($file); + $json = new JsonFile($file, new RemoteFilesystem($this->getIO())); $json->read(); $json->validateSchema(JsonFile::LAX_SCHEMA); diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 958844930..2815f22b7 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -16,6 +16,7 @@ use Composer\Json\JsonFile; use Composer\IO\IOInterface; use Composer\Repository\RepositoryManager; use Composer\Util\ProcessExecutor; +use Composer\Util\RemoteFilesystem; /** * Creates an configured instance of composer. @@ -38,7 +39,7 @@ class Factory $composerFile = getenv('COMPOSER') ?: 'composer.json'; } - $file = new JsonFile($composerFile); + $file = new JsonFile($composerFile, new RemoteFilesystem($io)); if (!$file->exists()) { if ($composerFile === 'composer.json') { $message = 'Composer could not find a composer.json file in '.getcwd(); @@ -98,7 +99,7 @@ class Factory // init locker $lockFile = substr($composerFile, -5) === '.json' ? substr($composerFile, 0, -4).'lock' : $composerFile . '.lock'; - $locker = new Package\Locker(new JsonFile($lockFile), $rm, md5_file($composerFile)); + $locker = new Package\Locker(new JsonFile($lockFile, new RemoteFilesystem($io)), $rm, md5_file($composerFile)); // initialize composer $composer = new Composer(); diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php index 458265e3e..5c891814e 100644 --- a/src/Composer/Json/JsonFile.php +++ b/src/Composer/Json/JsonFile.php @@ -17,6 +17,7 @@ use Composer\Composer; use JsonSchema\Validator; use Seld\JsonLint\JsonParser; use Composer\Util\StreamContextFactory; +use Composer\Util\RemoteFilesystem; /** * Reads/writes json files. @@ -34,15 +35,22 @@ class JsonFile const JSON_UNESCAPED_UNICODE = 256; private $path; + private $rfs; /** * Initializes json file reader/parser. * * @param string $lockFile path to a lockfile + * @param RemoteFilesystem $rfs required for loading http/https json files */ - public function __construct($path) + public function __construct($path, RemoteFilesystem $rfs = null) { $this->path = $path; + + if (null === $rfs && preg_match('{^https?://}i', $path)) { + throw new \InvalidArgumentException('http urls require a RemoteFilesystem instance to be passed'); + } + $this->rfs = $rfs; } public function getPath() @@ -67,15 +75,14 @@ class JsonFile */ public function read() { - $ctx = StreamContextFactory::getContext(array( - 'http' => array( - 'header' => 'User-Agent: Composer/'.Composer::VERSION."\r\n" - ) - )); - - $json = file_get_contents($this->path, false, $ctx); - if (!$json) { - throw new \RuntimeException('Could not read '.$this->path.', you are probably offline'); + try { + if ($this->rfs) { + $json = $this->rfs->getContents($this->path, $this->path, false); + } else { + $json = file_get_contents($this->path); + } + } catch (\Exception $e) { + throw new \RuntimeException('Could not read '.$this->path.', you are probably offline ('.$e->getMessage().')'); } return static::parseJson($json); diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 03a13b8e2..c05af141d 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -15,6 +15,8 @@ namespace Composer\Repository; use Composer\Package\Loader\ArrayLoader; use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Json\JsonFile; +use Composer\IO\IOInterface; +use Composer\Util\RemoteFilesystem; /** * @author Jordi Boggiano @@ -22,9 +24,10 @@ use Composer\Json\JsonFile; class ComposerRepository extends ArrayRepository { protected $url; + protected $io; protected $packages; - public function __construct(array $config) + public function __construct(array $config, IOInterface $io) { if (!preg_match('{^\w+://}', $config['url'])) { // assume http as the default protocol @@ -36,12 +39,13 @@ class ComposerRepository extends ArrayRepository } $this->url = $config['url']; + $this->io = $io; } protected function initialize() { parent::initialize(); - $json = new JsonFile($this->url.'/packages.json'); + $json = new JsonFile($this->url.'/packages.json', new RemoteFilesystem($this->io)); $packages = $json->read(); if (!$packages) { throw new \UnexpectedValueException('Could not parse package list from the '.$this->url.' repository'); diff --git a/src/Composer/Repository/PearRepository.php b/src/Composer/Repository/PearRepository.php index a1a2f4272..44a454128 100644 --- a/src/Composer/Repository/PearRepository.php +++ b/src/Composer/Repository/PearRepository.php @@ -13,7 +13,7 @@ namespace Composer\Repository; use Composer\Package\Loader\ArrayLoader; -use Composer\Util\StreamContextFactory; +use Composer\Util\RemoteFilesystem; /** * @author Benjamin Eberlei @@ -23,9 +23,9 @@ class PearRepository extends ArrayRepository { private $url; private $channel; - private $streamContext; + private $rfs; - public function __construct(array $config) + public function __construct(array $config, IOInterface $io, RemoteFilesystem $rfs = null) { if (!preg_match('{^https?://}', $config['url'])) { $config['url'] = 'http://'.$config['url']; @@ -36,8 +36,8 @@ class PearRepository extends ArrayRepository } $this->url = rtrim($config['url'], '/'); - $this->channel = !empty($config['channel']) ? $config['channel'] : null; + $this->rfs = $rfs ?: new RemoteFilesystem($io); } protected function initialize() @@ -47,7 +47,6 @@ class PearRepository extends ArrayRepository set_error_handler(function($severity, $message, $file, $line) { throw new \ErrorException($message, $severity, $severity, $file, $line); }); - $this->streamContext = StreamContextFactory::getContext(); $this->fetchFromServer(); restore_error_handler(); } @@ -120,7 +119,7 @@ class PearRepository extends ArrayRepository ); try { - $deps = file_get_contents($releaseLink . "/deps.".$pearVersion.".txt", false, $this->streamContext); + $deps = $this->rfs->getContents($this->url, $releaseLink . "/deps.".$pearVersion.".txt", false); } catch (\ErrorException $e) { if (strpos($e->getMessage(), '404')) { continue; @@ -289,7 +288,7 @@ class PearRepository extends ArrayRepository */ private function requestXml($url) { - $content = file_get_contents($url, false, $this->streamContext); + $content = $this->rfs->getContents($this->url, $url, false); if (!$content) { throw new \UnexpectedValueException('The PEAR channel at '.$url.' did not respond.'); } From f98bd971f23d9c723e4470039ba2888fcaae7821 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sun, 18 Mar 2012 21:05:10 +0100 Subject: [PATCH 3/5] Add Gzip handling to RemoteFilesystem --- src/Composer/Downloader/FileDownloader.php | 1 - src/Composer/Util/RemoteFilesystem.php | 44 ++++++++++++++----- .../Test/Util/RemoteFilesystemTest.php | 3 +- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 1b440048f..d2bfc845b 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -74,7 +74,6 @@ class FileDownloader implements DownloaderInterface $url = $this->processUrl($url); $this->rfs->copy($package->getSourceUrl(), $url, $fileName); - $this->io->write(''); if (!file_exists($fileName)) { throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the' diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index e14201a13..6c70a2f14 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -12,6 +12,7 @@ namespace Composer\Util; +use Composer\Composer; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; @@ -101,22 +102,39 @@ class RemoteFilesystem } $result = @file_get_contents($fileUrl, false, $ctx); - if (null !== $fileName) { - $result = @file_put_contents($fileName, $result) ? true : false; - } // fix for 5.4.0 https://bugs.php.net/bug.php?id=61336 if (!empty($http_response_header[0]) && preg_match('{^HTTP/\S+ 404}i', $http_response_header[0])) { $result = false; } + // decode gzip + if (false !== $result && extension_loaded('zlib') && substr($fileUrl, 0, 4) === 'http') { + foreach ($http_response_header as $header) { + if (preg_match('{^content-encoding: *gzip *$}i', $header)) { + if (version_compare(PHP_VERSION, '5.4.0', '>=')) { + $result = zlib_decode($result); + } else { + // work around issue with gzuncompress & co that do not work with all gzip checksums + $result = file_get_contents('compress.zlib://data:application/octet-stream;base64,'.base64_encode($result)); + } + break; + } + } + } + + // handle copy command if download was successful + if (false !== $result && null !== $fileName) { + $result = (Boolean) @file_put_contents($fileName, $result); + } + // avoid overriding if content was loaded by a sub-call to get() if (null === $this->result) { $this->result = $result; } if ($this->progress) { - $this->io->overwrite(" Downloading", false); + $this->io->write(''); } if (false === $this->result) { @@ -184,17 +202,21 @@ class RemoteFilesystem } } - protected function getOptionsForUrl($url) + protected function getOptionsForUrl($originUrl) { - $options = array(); - if ($this->io->hasAuthorization($url)) { - $auth = $this->io->getAuthorization($url); + $options['http']['header'] = 'User-Agent: Composer/'.Composer::VERSION."\r\n"; + if (extension_loaded('zlib')) { + $options['http']['header'] .= 'Accept-Encoding: gzip'."\r\n"; + } + + if ($this->io->hasAuthorization($originUrl)) { + $auth = $this->io->getAuthorization($originUrl); $authStr = base64_encode($auth['username'] . ':' . $auth['password']); - $options['http'] = array('header' => "Authorization: Basic $authStr\r\n"); + $options['http']['header'] .= "Authorization: Basic $authStr\r\n"; } elseif (null !== $this->io->getLastUsername()) { $authStr = base64_encode($this->io->getLastUsername() . ':' . $this->io->getLastPassword()); - $options['http'] = array('header' => "Authorization: Basic $authStr\r\n"); - $this->io->setAuthorization($url, $this->io->getLastUsername(), $this->io->getLastPassword()); + $options['http']['header'] .= "Authorization: Basic $authStr\r\n"; + $this->io->setAuthorization($originUrl, $this->io->getLastUsername(), $this->io->getLastPassword()); } return $options; diff --git a/tests/Composer/Test/Util/RemoteFilesystemTest.php b/tests/Composer/Test/Util/RemoteFilesystemTest.php index 97e50b848..e196fa3f4 100644 --- a/tests/Composer/Test/Util/RemoteFilesystemTest.php +++ b/tests/Composer/Test/Util/RemoteFilesystemTest.php @@ -31,7 +31,8 @@ class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue(null)) ; - $this->assertEquals(array(), $this->callGetOptionsForUrl($io, array('http://example.org'))); + $res = $this->callGetOptionsForUrl($io, array('http://example.org')); + $this->assertTrue(isset($res['http']['header']) && false !== strpos($res['http']['header'], 'User-Agent'), 'getOptions must return an array with a header containing a User-Agent'); } public function testGetOptionsForUrlWithAuthorization() From e4cce193cf513c76f0dd3d18d1cce74b565aec24 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sun, 18 Mar 2012 22:12:25 +0100 Subject: [PATCH 4/5] Fix PEAR repository --- src/Composer/Repository/PearRepository.php | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Composer/Repository/PearRepository.php b/src/Composer/Repository/PearRepository.php index 44a454128..8b48d2db5 100644 --- a/src/Composer/Repository/PearRepository.php +++ b/src/Composer/Repository/PearRepository.php @@ -12,8 +12,10 @@ namespace Composer\Repository; +use Composer\IO\IOInterface; use Composer\Package\Loader\ArrayLoader; use Composer\Util\RemoteFilesystem; +use Composer\Downloader\TransportException; /** * @author Benjamin Eberlei @@ -23,6 +25,7 @@ class PearRepository extends ArrayRepository { private $url; private $channel; + private $io; private $rfs; public function __construct(array $config, IOInterface $io, RemoteFilesystem $rfs = null) @@ -37,18 +40,16 @@ class PearRepository extends ArrayRepository $this->url = rtrim($config['url'], '/'); $this->channel = !empty($config['channel']) ? $config['channel'] : null; - $this->rfs = $rfs ?: new RemoteFilesystem($io); + $this->io = $io; + $this->rfs = $rfs ?: new RemoteFilesystem($this->io); } protected function initialize() { parent::initialize(); - set_error_handler(function($severity, $message, $file, $line) { - throw new \ErrorException($message, $severity, $severity, $file, $line); - }); + $this->io->write('Initializing PEAR repository '.$this->url); $this->fetchFromServer(); - restore_error_handler(); } protected function fetchFromServer() @@ -67,7 +68,7 @@ class PearRepository extends ArrayRepository try { $packagesLink = str_replace("info.xml", "packagesinfo.xml", $link); $this->fetchPear2Packages($this->url . $packagesLink); - } catch (\ErrorException $e) { + } catch (TransportException $e) { if (false === strpos($e->getMessage(), '404')) { throw $e; } @@ -80,7 +81,7 @@ class PearRepository extends ArrayRepository /** * @param string $categoryLink - * @throws ErrorException + * @throws TransportException * @throws InvalidArgumentException */ private function fetchPearPackages($categoryLink) @@ -98,7 +99,7 @@ class PearRepository extends ArrayRepository try { $releasesXML = $this->requestXml($allReleasesLink); - } catch (\ErrorException $e) { + } catch (TransportException $e) { if (strpos($e->getMessage(), '404')) { continue; } @@ -120,7 +121,7 @@ class PearRepository extends ArrayRepository try { $deps = $this->rfs->getContents($this->url, $releaseLink . "/deps.".$pearVersion.".txt", false); - } catch (\ErrorException $e) { + } catch (TransportException $e) { if (strpos($e->getMessage(), '404')) { continue; } @@ -225,6 +226,7 @@ class PearRepository extends ArrayRepository { $loader = new ArrayLoader(); $packagesXml = $this->requestXml($packagesLink); + $informations = $packagesXml->getElementsByTagName('pi'); foreach ($informations as $information) { $package = $information->getElementsByTagName('p')->item(0); From 22149d3a70c726c7709a221bfc62f65b18049bb9 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sun, 18 Mar 2012 22:12:48 +0100 Subject: [PATCH 5/5] Fix gzip decoding after a redirect --- src/Composer/Util/RemoteFilesystem.php | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 6c70a2f14..a77b6e63e 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -110,15 +110,22 @@ class RemoteFilesystem // decode gzip if (false !== $result && extension_loaded('zlib') && substr($fileUrl, 0, 4) === 'http') { + $decode = false; foreach ($http_response_header as $header) { if (preg_match('{^content-encoding: *gzip *$}i', $header)) { - if (version_compare(PHP_VERSION, '5.4.0', '>=')) { - $result = zlib_decode($result); - } else { - // work around issue with gzuncompress & co that do not work with all gzip checksums - $result = file_get_contents('compress.zlib://data:application/octet-stream;base64,'.base64_encode($result)); - } - break; + $decode = true; + continue; + } elseif (preg_match('{^HTTP/}i', $header)) { + $decode = false; + } + } + + if ($decode) { + if (version_compare(PHP_VERSION, '5.4.0', '>=')) { + $result = zlib_decode($result); + } else { + // work around issue with gzuncompress & co that do not work with all gzip checksums + $result = file_get_contents('compress.zlib://data:application/octet-stream;base64,'.base64_encode($result)); } } } @@ -138,7 +145,7 @@ class RemoteFilesystem } if (false === $this->result) { - throw new TransportException("The '$fileUrl' file could not be downloaded"); + throw new TransportException('The "'.$fileUrl.'" file could not be downloaded'); } } @@ -156,7 +163,7 @@ class RemoteFilesystem { switch ($notificationCode) { case STREAM_NOTIFY_FAILURE: - throw new TransportException(trim($message), $messageCode); + throw new TransportException('The "'.$this->fileUrl.'" file could not be downloaded ('.trim($message).')', $messageCode); break; case STREAM_NOTIFY_AUTH_REQUIRED: