ソースを参照

Merge pull request #473 from Seldaek/gzip

Automatic gzip encoding support
Nils Adermann 13 年 前
コミット
366e98288e

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

+ 2 - 1
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 {

+ 4 - 5
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 <info>%s</info>.", $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("<info>You are using the latest composer version.</info>");
         }

+ 2 - 1
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 <seroscho@googlemail.com>
@@ -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);

+ 0 - 1
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'

+ 3 - 2
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();

+ 17 - 10
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);

+ 6 - 2
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 <j.boggiano@seld.be>
@@ -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');

+ 16 - 15
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 <kontakt@beberlei.de>
@@ -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.');
         }

+ 42 - 13
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;

+ 2 - 1
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()