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(); + } } 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/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/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..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\StreamContextFactory; +use Composer\Util\RemoteFilesystem; +use Composer\Downloader\TransportException; /** * @author Benjamin Eberlei @@ -23,9 +25,10 @@ class PearRepository extends ArrayRepository { private $url; private $channel; - private $streamContext; + private $io; + 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,20 +39,17 @@ class PearRepository extends ArrayRepository } $this->url = rtrim($config['url'], '/'); - $this->channel = !empty($config['channel']) ? $config['channel'] : null; + $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->streamContext = StreamContextFactory::getContext(); + $this->io->write('Initializing PEAR repository '.$this->url); $this->fetchFromServer(); - restore_error_handler(); } protected function fetchFromServer() @@ -68,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; } @@ -81,7 +81,7 @@ class PearRepository extends ArrayRepository /** * @param string $categoryLink - * @throws ErrorException + * @throws TransportException * @throws InvalidArgumentException */ private function fetchPearPackages($categoryLink) @@ -99,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,8 +120,8 @@ class PearRepository extends ArrayRepository ); try { - $deps = file_get_contents($releaseLink . "/deps.".$pearVersion.".txt", false, $this->streamContext); - } catch (\ErrorException $e) { + $deps = $this->rfs->getContents($this->url, $releaseLink . "/deps.".$pearVersion.".txt", false); + } catch (TransportException $e) { if (strpos($e->getMessage(), '404')) { continue; } @@ -226,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); @@ -289,7 +290,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.'); } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index e14201a13..a77b6e63e 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,26 +102,50 @@ 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') { + $decode = false; + foreach ($http_response_header as $header) { + if (preg_match('{^content-encoding: *gzip *$}i', $header)) { + $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)); + } + } + } + + // 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) { - throw new TransportException("The '$fileUrl' file could not be downloaded"); + throw new TransportException('The "'.$fileUrl.'" file could not be downloaded'); } } @@ -138,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: @@ -184,17 +209,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()