Przeglądaj źródła

Merge remote-tracking branch 'hason/filedownloader'

Jordi Boggiano 13 lat temu
rodzic
commit
9bcea6f485

+ 99 - 0
src/Composer/Downloader/ArchiveDownloader.php

@@ -0,0 +1,99 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Downloader;
+
+use Composer\IO\IOInterface;
+use Composer\Package\PackageInterface;
+use Composer\Util\Filesystem;
+use Composer\Util\RemoteFilesystem;
+
+/**
+ * Base downloader for archives
+ *
+ * @author Kirill chEbba Chebunin <iam@chebba.org>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author François Pluchino <francois.pluchino@opendisplay.com>
+ */
+abstract class ArchiveDownloader extends FileDownloader
+{
+    /**
+     * {@inheritDoc}
+     */
+    public function download(PackageInterface $package, $path)
+    {
+        parent::download($package, $path);
+
+        $fileName = $this->getFileName($package, $path);
+        $this->io->write('    Unpacking archive');
+        $this->extract($fileName, $path);
+
+        $this->io->write('    Cleaning up');
+        unlink($fileName);
+
+        // If we have only a one dir inside it suppose to be a package itself
+        $contentDir = glob($path . '/*');
+        if (1 === count($contentDir)) {
+            $contentDir = $contentDir[0];
+
+            // Rename the content directory to avoid error when moving up
+            // a child folder with the same name
+            $temporaryName = md5(time().rand());
+            rename($contentDir, $temporaryName);
+            $contentDir = $temporaryName;
+
+            foreach (array_merge(glob($contentDir . '/.*'), glob($contentDir . '/*')) as $file) {
+                if (trim(basename($file), '.')) {
+                    rename($file, $path . '/' . basename($file));
+                }
+            }
+            rmdir($contentDir);
+        }
+
+        $this->io->write('');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function getFileName(PackageInterface $package, $path)
+    {
+        return rtrim($path.'/'.md5($path.spl_object_hash($package)).'.'.pathinfo($package->getDistUrl(), PATHINFO_EXTENSION), '.');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function processUrl($url)
+    {
+        if (!extension_loaded('openssl') && (0 === strpos($url, 'https:') || 0 === strpos($url, 'http://github.com'))) {
+            // bypass https for github if openssl is disabled
+            if (preg_match('{^https?://(github.com/[^/]+/[^/]+/(zip|tar)ball/[^/]+)$}i', $url, $match)) {
+                $url = 'http://nodeload.'.$match[1];
+            } else {
+                throw new \RuntimeException('You must enable the openssl extension to download files via https');
+            }
+        }
+
+        return $url;
+    }
+
+    /**
+     * Extract file to directory
+     *
+     * @param string $file Extracted file
+     * @param string $path Directory
+     *
+     * @throws \UnexpectedValueException If can not extract downloaded file to path
+     */
+    abstract protected function extract($file, $path);
+}

+ 34 - 45
src/Composer/Downloader/FileDownloader.php

@@ -18,13 +18,13 @@ use Composer\Util\Filesystem;
 use Composer\Util\RemoteFilesystem;
 
 /**
- * Base downloader for file packages
+ * Base downloader for files
  *
  * @author Kirill chEbba Chebunin <iam@chebba.org>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  * @author François Pluchino <francois.pluchino@opendisplay.com>
  */
-abstract class FileDownloader implements DownloaderInterface
+class FileDownloader implements DownloaderInterface
 {
     protected $io;
 
@@ -52,7 +52,9 @@ abstract class FileDownloader implements DownloaderInterface
     public function download(PackageInterface $package, $path)
     {
         $url = $package->getDistUrl();
-        $checksum = $package->getDistSha1Checksum();
+        if (!$url) {
+            throw new \InvalidArgumentException('The given package is missing url information');
+        }
 
         if (!is_dir($path)) {
             if (file_exists($path)) {
@@ -63,18 +65,11 @@ abstract class FileDownloader implements DownloaderInterface
             }
         }
 
-        $fileName = rtrim($path.'/'.md5(time().rand()).'.'.pathinfo($url, PATHINFO_EXTENSION), '.');
+        $fileName = $this->getFileName($package, $path);
 
         $this->io->write("  - Package <info>" . $package->getName() . "</info> (<comment>" . $package->getPrettyVersion() . "</comment>)");
 
-        if (!extension_loaded('openssl') && (0 === strpos($url, 'https:') || 0 === strpos($url, 'http://github.com'))) {
-            // bypass https for github if openssl is disabled
-            if (preg_match('{^https?://(github.com/[^/]+/[^/]+/(zip|tar)ball/[^/]+)$}i', $url, $match)) {
-                $url = 'http://nodeload.'.$match[1];
-            } else {
-                throw new \RuntimeException('You must enable the openssl extension to download files via https');
-            }
-        }
+        $url = $this->processUrl($url);
 
         $rfs = new RemoteFilesystem($this->io);
         $rfs->copy($package->getSourceUrl(), $url, $fileName);
@@ -85,33 +80,9 @@ abstract class FileDownloader implements DownloaderInterface
                 .' directory is writable and you have internet connectivity');
         }
 
+        $checksum = $package->getDistSha1Checksum();
         if ($checksum && hash_file('sha1', $fileName) !== $checksum) {
-            throw new \UnexpectedValueException('The checksum verification of the archive failed (downloaded from '.$url.')');
-        }
-
-        $this->io->write('    Unpacking archive');
-        $this->extract($fileName, $path);
-
-        $this->io->write('    Cleaning up');
-        unlink($fileName);
-
-        // If we have only a one dir inside it suppose to be a package itself
-        $contentDir = glob($path . '/*');
-        if (1 === count($contentDir)) {
-            $contentDir = $contentDir[0];
-
-            // Rename the content directory to avoid error when moving up
-            // a child folder with the same name
-            $temporaryName = md5(time().rand());
-            rename($contentDir, $temporaryName);
-            $contentDir = $temporaryName;
-
-            foreach (array_merge(glob($contentDir . '/.*'), glob($contentDir . '/*')) as $file) {
-                if (trim(basename($file), '.')) {
-                    rename($file, $path . '/' . basename($file));
-                }
-            }
-            rmdir($contentDir);
+            throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url.')');
         }
 
         $this->io->write('');
@@ -122,8 +93,7 @@ abstract class FileDownloader implements DownloaderInterface
      */
     public function update(PackageInterface $initial, PackageInterface $target, $path)
     {
-        $fs = new Filesystem();
-        $fs->removeDirectory($path);
+        $this->remove($initial, $path);
         $this->download($target, $path);
     }
 
@@ -137,12 +107,31 @@ abstract class FileDownloader implements DownloaderInterface
     }
 
     /**
-     * Extract file to directory
+     * Gets file name for specific package
+     *
+     * @param  PackageInterface $package   package instance
+     * @param  string           $path      download path
+     * @return string file name
+     */
+    protected function getFileName(PackageInterface $package, $path)
+    {
+        return $path.'/'.pathinfo($package->getDistUrl(), PATHINFO_BASENAME);
+    }
+
+    /**
+     * Process the download url
      *
-     * @param string $file Extracted file
-     * @param string $path Directory
+     * @param  string           $url       download url
+     * @return string url
      *
-     * @throws \UnexpectedValueException If can not extract downloaded file to path
+     * @throws \RuntimeException If any problem with the url
      */
-    protected abstract function extract($file, $path);
+    protected function processUrl($url)
+    {
+        if (!extension_loaded('openssl') && 0 === strpos($url, 'https:')) {
+            throw new \RuntimeException('You must enable the openssl extension to download files via https');
+        }
+
+        return $url;
+    }
 }

+ 1 - 1
src/Composer/Downloader/PharDownloader.php

@@ -19,7 +19,7 @@ use Composer\Package\PackageInterface;
  *
  * @author Kirill chEbba Chebunin <iam@chebba.org>
  */
-class PharDownloader extends FileDownloader
+class PharDownloader extends ArchiveDownloader
 {
     /**
      * {@inheritDoc}

+ 1 - 1
src/Composer/Downloader/TarDownloader.php

@@ -19,7 +19,7 @@ use Composer\Package\PackageInterface;
  *
  * @author Kirill chEbba Chebunin <iam@chebba.org>
  */
-class TarDownloader extends FileDownloader
+class TarDownloader extends ArchiveDownloader
 {
     /**
      * {@inheritDoc}

+ 1 - 1
src/Composer/Downloader/ZipDownloader.php

@@ -19,7 +19,7 @@ use Composer\IO\IOInterface;
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
  */
-class ZipDownloader extends FileDownloader
+class ZipDownloader extends ArchiveDownloader
 {
     protected $process;
 

+ 6 - 5
src/Composer/Factory.php

@@ -141,13 +141,14 @@ class Factory
     protected function createDownloadManager(IOInterface $io)
     {
         $dm = new Downloader\DownloadManager();
-        $dm->setDownloader('git',  new Downloader\GitDownloader($io));
-        $dm->setDownloader('svn',  new Downloader\SvnDownloader($io));
+        $dm->setDownloader('git', new Downloader\GitDownloader($io));
+        $dm->setDownloader('svn', new Downloader\SvnDownloader($io));
         $dm->setDownloader('hg', new Downloader\HgDownloader($io));
         $dm->setDownloader('pear', new Downloader\PearDownloader($io));
-        $dm->setDownloader('zip',  new Downloader\ZipDownloader($io));
-        $dm->setDownloader('tar',  new Downloader\TarDownloader($io));
-        $dm->setDownloader('phar',  new Downloader\PharDownloader($io));
+        $dm->setDownloader('zip', new Downloader\ZipDownloader($io));
+        $dm->setDownloader('tar', new Downloader\TarDownloader($io));
+        $dm->setDownloader('phar', new Downloader\PharDownloader($io));
+        $dm->setDownloader('file', new Downloader\FileDownloader($io));
 
         return $dm;
     }

+ 33 - 32
src/Composer/Util/RemoteFilesystem.php

@@ -26,7 +26,7 @@ class RemoteFilesystem
     private $fileUrl;
     private $fileName;
     private $result;
-    private $progess;
+    private $progress;
     private $lastProgress;
 
     /**
@@ -45,13 +45,13 @@ class RemoteFilesystem
      * @param string  $originUrl The orgin URL
      * @param string  $fileUrl   The file URL
      * @param string  $fileName  the local filename
-     * @param boolean $progess   Display the progression
+     * @param boolean $progress  Display the progression
      *
      * @return Boolean true
      */
-    public function copy($originUrl, $fileUrl, $fileName, $progess = true)
+    public function copy($originUrl, $fileUrl, $fileName, $progress = true)
     {
-        $this->get($originUrl, $fileUrl, $fileName, $progess);
+        $this->get($originUrl, $fileUrl, $fileName, $progress);
 
         return $this->result;
     }
@@ -61,13 +61,13 @@ class RemoteFilesystem
      *
      * @param string  $originUrl The orgin URL
      * @param string  $fileUrl   The file URL
-     * @param boolean $progess   Display the progression
+     * @param boolean $progress  Display the progression
      *
      * @return string The content
      */
-    public function getContents($originUrl, $fileUrl, $progess = true)
+    public function getContents($originUrl, $fileUrl, $progress = true)
     {
-        $this->get($originUrl, $fileUrl, null, $progess);
+        $this->get($originUrl, $fileUrl, null, $progress);
 
         return $this->result;
     }
@@ -78,12 +78,12 @@ class RemoteFilesystem
      * @param string  $originUrl The orgin URL
      * @param string  $fileUrl   The file URL
      * @param string  $fileName  the local filename
-     * @param boolean $progess   Display the progression
+     * @param boolean $progress  Display the progression
      * @param boolean $firstCall Whether this is the first attempt at fetching this resource
      *
      * @throws \RuntimeException When the file could not be downloaded
      */
-    protected function get($originUrl, $fileUrl, $fileName = null, $progess = true, $firstCall = true)
+    protected function get($originUrl, $fileUrl, $fileName = null, $progress = true, $firstCall = true)
     {
         $this->firstCall = $firstCall;
         $this->bytesMax = 0;
@@ -91,21 +91,10 @@ class RemoteFilesystem
         $this->originUrl = $originUrl;
         $this->fileUrl = $fileUrl;
         $this->fileName = $fileName;
-        $this->progress = $progess;
+        $this->progress = $progress;
         $this->lastProgress = null;
 
-        // add authorization in context
-        $options = array();
-        if ($this->io->hasAuthorization($originUrl)) {
-            $auth = $this->io->getAuthorization($originUrl);
-            $authStr = base64_encode($auth['username'] . ':' . $auth['password']);
-            $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($originUrl, $this->io->getLastUsername(), $this->io->getLastPassword());
-        }
-
+        $options = $this->getOptionsForUrl($originUrl);
         $ctx = StreamContextFactory::getContext($options, array('notification' => array($this, 'callbackGet')));
 
         if ($this->progress) {
@@ -147,24 +136,20 @@ class RemoteFilesystem
         switch ($notificationCode) {
             case STREAM_NOTIFY_AUTH_REQUIRED:
             case STREAM_NOTIFY_FAILURE:
-                // for private repository returning 404 error when the authorization is incorrect
-                $auth = $this->io->getAuthorization($this->originUrl);
-                $attemptAuthentication = $this->firstCall && 404 === $messageCode && null === $auth['username'];
-
                 if (404 === $messageCode && !$this->firstCall) {
                     throw new \RuntimeException("The '" . $this->fileUrl . "' URL not found");
                 }
 
+                // for private repository returning 404 error when the authorization is incorrect
+                $auth = $this->io->getAuthorization($this->originUrl);
+                $attemptAuthentication = $this->firstCall && 404 === $messageCode && null === $auth['username'];
+
                 $this->firstCall = false;
 
                 // get authorization informations
                 if (401 === $messageCode || $attemptAuthentication) {
                     if (!$this->io->isInteractive()) {
-                        $mess = "The '" . $this->fileUrl . "' URL was not found";
-
-                        if (401 === $code || $attemptAuthentication) {
-                            $mess = "The '" . $this->fileUrl . "' URL required authentication.\nYou must be using the interactive console";
-                        }
+                        $mess = "The '" . $this->fileUrl . "' URL required authentication.\nYou must be using the interactive console";
 
                         throw new \RuntimeException($mess);
                     }
@@ -203,4 +188,20 @@ class RemoteFilesystem
                 break;
         }
     }
-}
+
+    protected function getOptionsForUrl($url)
+    {
+        $options = array();
+        if ($this->io->hasAuthorization($url)) {
+            $auth = $this->io->getAuthorization($url);
+            $authStr = base64_encode($auth['username'] . ':' . $auth['password']);
+            $options['http'] = array('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());
+        }
+
+        return $options;
+    }
+}

+ 35 - 0
tests/Composer/Test/Downloader/ArchiveDownloaderTest.php

@@ -0,0 +1,35 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Downloader;
+
+use Composer\Util\Filesystem;
+
+class ArchiveDownloaderTest extends \PHPUnit_Framework_TestCase
+{
+    public function testGetFileName()
+    {
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $packageMock->expects($this->any())
+            ->method('getDistUrl')
+            ->will($this->returnValue('http://example.com/script.js'))
+        ;
+
+        $downloader = $this->getMockForAbstractClass('Composer\Downloader\ArchiveDownloader', array($this->getMock('Composer\IO\IOInterface')));
+        $method = new \ReflectionMethod($downloader, 'getFileName');
+        $method->setAccessible(true);
+
+        $first = $method->invoke($downloader, $packageMock, '/path');
+        $this->assertRegExp('#/path/[a-z0-9]+\.js#', $first);
+        $this->assertSame($first, $method->invoke($downloader, $packageMock, '/path'));
+    }
+}

+ 145 - 0
tests/Composer/Test/Downloader/FileDownloaderTest.php

@@ -0,0 +1,145 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Downloader;
+
+use Composer\Downloader\FileDownloader;
+use Composer\Util\Filesystem;
+
+class FileDownloaderTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @expectedException \InvalidArgumentException
+     */
+    public function testDownloadForPackageWithoutDistReference()
+    {
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $packageMock->expects($this->once())
+            ->method('getDistUrl')
+            ->will($this->returnValue(null))
+        ;
+
+        $downloader = new FileDownloader($this->getMock('Composer\IO\IOInterface'));
+        $downloader->download($packageMock, '/path');
+    }
+
+    public function testDownloadToExistFile()
+    {
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $packageMock->expects($this->once())
+            ->method('getDistUrl')
+            ->will($this->returnValue('url'))
+        ;
+
+        $path = tempnam(sys_get_temp_dir(), 'c');
+
+        $downloader = new FileDownloader($this->getMock('Composer\IO\IOInterface'));
+        try {
+            $downloader->download($packageMock, $path);
+            $this->fail();
+        } catch (\Exception $e) {
+            if (file_exists($path)) {
+                unset($path);
+            }
+            $this->assertInstanceOf('UnexpectedValueException', $e);
+            $this->assertContains('exists and is not a directory', $e->getMessage());
+        }
+    }
+
+    public function testGetFileName()
+    {
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $packageMock->expects($this->once())
+            ->method('getDistUrl')
+            ->will($this->returnValue('http://example.com/script.js'))
+        ;
+
+        $downloader = new FileDownloader($this->getMock('Composer\IO\IOInterface'));
+        $method = new \ReflectionMethod($downloader, 'getFileName');
+        $method->setAccessible(true);
+
+        $this->assertEquals('/path/script.js', $method->invoke($downloader, $packageMock, '/path'));
+    }
+
+    public function testDownloadButFileIsUnsaved()
+    {
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $packageMock->expects($this->any())
+            ->method('getDistUrl')
+            ->will($this->returnValue('http://example.com/script.js'))
+        ;
+
+        do {
+            $path = sys_get_temp_dir().'/'.md5(time().rand());
+        } while (file_exists($path));
+
+        $ioMock = $this->getMock('Composer\IO\IOInterface');
+        $ioMock->expects($this->any())
+            ->method('write')
+            ->will($this->returnCallback(function($messages, $newline = true) use ($path) {
+                if (is_file($path.'/script.js')) {
+                    unlink($path.'/script.js');
+                }
+                return $messages;
+            }))
+        ;
+
+        $downloader = new FileDownloader($ioMock);
+        try {
+            $downloader->download($packageMock, $path);
+            $this->fail();
+        } catch (\Exception $e) {
+            if (is_dir($path)) {
+                $fs = new Filesystem();
+                $fs->removeDirectory($path);
+            } else if (is_file($path)) {
+                unset($path);
+            }
+
+            $this->assertInstanceOf('UnexpectedValueException', $e);
+            $this->assertContains('could not be saved to', $e->getMessage());
+        }
+    }
+
+    public function testDownloadFileWithInvalidChecksum()
+    {
+        $packageMock = $this->getMock('Composer\Package\PackageInterface');
+        $packageMock->expects($this->any())
+            ->method('getDistUrl')
+            ->will($this->returnValue('http://example.com/script.js'))
+        ;
+        $packageMock->expects($this->any())
+            ->method('getDistSha1Checksum')
+            ->will($this->returnValue('invalid'))
+        ;
+
+        do {
+            $path = sys_get_temp_dir().'/'.md5(time().rand());
+        } while (file_exists($path));
+
+        $downloader = new FileDownloader($this->getMock('Composer\IO\IOInterface'));
+        try {
+            $downloader->download($packageMock, $path);
+            $this->fail();
+        } catch (\Exception $e) {
+            if (is_dir($path)) {
+                $fs = new Filesystem();
+                $fs->removeDirectory($path);
+            } else if (is_file($path)) {
+                unset($path);
+            }
+
+            $this->assertInstanceOf('UnexpectedValueException', $e);
+            $this->assertContains('checksum verification', $e->getMessage());
+        }
+    }
+}

+ 1 - 4
tests/Composer/Test/Downloader/Util/FilesystemTest.php → tests/Composer/Test/Util/FilesystemTest.php

@@ -10,7 +10,7 @@
  * file that was distributed with this source code.
  */
 
-namespace Composer\Test\Repository;
+namespace Composer\Test\Util;
 
 use Composer\Util\Filesystem;
 use Composer\Test\TestCase;
@@ -32,7 +32,6 @@ class FilesystemTest extends TestCase
             array('/foo/bar', '/foo/bar', false, "__FILE__"),
             array('/foo/bar', '/foo/baz', false, "__DIR__.'/baz'"),
             array('/foo/bin/run', '/foo/vendor/acme/bin/run', false, "dirname(__DIR__).'/vendor/acme/bin/run'"),
-            array('/foo/bin/run', '/foo/vendor/acme/bin/run', false, "dirname(__DIR__).'/vendor/acme/bin/run'"),
             array('/foo/bin/run', '/bar/bin/run', false, "'/bar/bin/run'"),
             array('c:/bin/run', 'c:/vendor/acme/bin/run', false, "dirname(__DIR__).'/vendor/acme/bin/run'"),
             array('c:\\bin\\run', 'c:/vendor/acme/bin/run', false, "dirname(__DIR__).'/vendor/acme/bin/run'"),
@@ -41,7 +40,6 @@ class FilesystemTest extends TestCase
             array('/foo/bar', '/foo/bar', true, "__DIR__"),
             array('/foo/bar', '/foo/baz', true, "dirname(__DIR__).'/baz'"),
             array('/foo/bin/run', '/foo/vendor/acme/bin/run', true, "dirname(dirname(__DIR__)).'/vendor/acme/bin/run'"),
-            array('/foo/bin/run', '/foo/vendor/acme/bin/run', true, "dirname(dirname(__DIR__)).'/vendor/acme/bin/run'"),
             array('/foo/bin/run', '/bar/bin/run', true, "'/bar/bin/run'"),
             array('c:/bin/run', 'c:/vendor/acme/bin/run', true, "dirname(dirname(__DIR__)).'/vendor/acme/bin/run'"),
             array('c:\\bin\\run', 'c:/vendor/acme/bin/run', true, "dirname(dirname(__DIR__)).'/vendor/acme/bin/run'"),
@@ -70,7 +68,6 @@ class FilesystemTest extends TestCase
             array('/foo/bar', '/foo/bar', "./bar"),
             array('/foo/bar', '/foo/baz', "./baz"),
             array('/foo/bin/run', '/foo/vendor/acme/bin/run', "../vendor/acme/bin/run"),
-            array('/foo/bin/run', '/foo/vendor/acme/bin/run', "../vendor/acme/bin/run"),
             array('/foo/bin/run', '/bar/bin/run', "/bar/bin/run"),
             array('c:/bin/run', 'c:/vendor/acme/bin/run', "../vendor/acme/bin/run"),
             array('c:\\bin\\run', 'c:/vendor/acme/bin/run', "../vendor/acme/bin/run"),

+ 186 - 0
tests/Composer/Test/Util/RemoteFilesystemTest.php

@@ -0,0 +1,186 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Util;
+
+use Composer\Util\RemoteFilesystem;
+use Composer\Test\TestCase;
+
+class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase
+{
+    public function testGetOptionsForUrl()
+    {
+        $io = $this->getMock('Composer\IO\IOInterface');
+        $io
+            ->expects($this->once())
+            ->method('hasAuthorization')
+            ->will($this->returnValue(false))
+        ;
+        $io
+            ->expects($this->once())
+            ->method('getLastUsername')
+            ->will($this->returnValue(null))
+        ;
+
+        $this->assertEquals(array(), $this->callGetOptionsForUrl($io, array('http://example.org')));
+    }
+
+    public function testGetOptionsForUrlWithAuthorization()
+    {
+        $io = $this->getMock('Composer\IO\IOInterface');
+        $io
+            ->expects($this->once())
+            ->method('hasAuthorization')
+            ->will($this->returnValue(true))
+        ;
+        $io
+            ->expects($this->once())
+            ->method('getAuthorization')
+            ->will($this->returnValue(array('username' => 'login', 'password' => 'password')))
+        ;
+
+        $options = $this->callGetOptionsForUrl($io, array('http://example.org'));
+        $this->assertContains('Authorization: Basic', $options['http']['header']);
+    }
+
+    public function testGetOptionsForUrlWithLastUsername()
+    {
+        $io = $this->getMock('Composer\IO\IOInterface');
+        $io
+            ->expects($this->once())
+            ->method('hasAuthorization')
+            ->will($this->returnValue(false))
+        ;
+        $io
+            ->expects($this->any())
+            ->method('getLastUsername')
+            ->will($this->returnValue('login'))
+        ;
+        $io
+            ->expects($this->any())
+            ->method('getLastPassword')
+            ->will($this->returnValue('password'))
+        ;
+        $io
+            ->expects($this->once())
+            ->method('setAuthorization')
+        ;
+
+        $options = $this->callGetOptionsForUrl($io, array('http://example.org'));
+        $this->assertContains('Authorization: Basic', $options['http']['header']);
+    }
+
+    public function testCallbackGetFileSize()
+    {
+        $fs = new RemoteFilesystem($this->getMock('Composer\IO\IOInterface'));
+        $this->callCallbackGet($fs, STREAM_NOTIFY_FILE_SIZE_IS, 0, '', 0, 0, 20);
+        $this->assertAttributeEquals(20, 'bytesMax', $fs);
+    }
+
+    public function testCallbackGetNotifyProgress()
+    {
+        $io = $this->getMock('Composer\IO\IOInterface');
+        $io
+            ->expects($this->once())
+            ->method('overwrite')
+        ;
+
+        $fs = new RemoteFilesystem($io);
+        $this->setAttribute($fs, 'bytesMax', 20);
+        $this->setAttribute($fs, 'progress', true);
+
+        $this->callCallbackGet($fs, STREAM_NOTIFY_PROGRESS, 0, '', 0, 10, 20);
+        $this->assertAttributeEquals(50, 'lastProgress', $fs);
+    }
+
+    public function testCallbackGetNotifyFailure404()
+    {
+        $fs = new RemoteFilesystem($this->getMock('Composer\IO\IOInterface'));
+        $this->setAttribute($fs, 'firstCall', false);
+
+        try {
+            $this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, '', 404, 0, 0);
+            $this->fail();
+        } catch (\Exception $e) {
+            $this->assertInstanceOf('RuntimeException', $e);
+            $this->assertContains('URL not found', $e->getMessage());
+        }
+    }
+
+    public function testCallbackGetNotifyFailure404FirstCall()
+    {
+        $io = $this->getMock('Composer\IO\IOInterface');
+        $io
+            ->expects($this->once())
+            ->method('getAuthorization')
+            ->will($this->returnValue(array('username' => null)))
+        ;
+        $io
+            ->expects($this->once())
+            ->method('isInteractive')
+            ->will($this->returnValue(false))
+        ;
+
+        $fs = new RemoteFilesystem($io);
+        $this->setAttribute($fs, 'firstCall', true);
+
+        try {
+            $this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, '', 404, 0, 0);
+            $this->fail();
+        } catch (\Exception $e) {
+            $this->assertInstanceOf('RuntimeException', $e);
+            $this->assertContains('URL required authentication', $e->getMessage());
+            $this->assertAttributeEquals(false, 'firstCall', $fs);
+        }
+    }
+
+    public function testGetContents()
+    {
+        $fs = new RemoteFilesystem($this->getMock('Composer\IO\IOInterface'));
+
+        $this->assertContains('RFC 2606', $fs->getContents('http://example.org', 'http://example.org'));
+    }
+
+    public function testCopy()
+    {
+        $fs = new RemoteFilesystem($this->getMock('Composer\IO\IOInterface'));
+
+        $file = tempnam(sys_get_temp_dir(), 'c');
+        $this->assertTrue($fs->copy('http://example.org', 'http://example.org', $file));
+        $this->assertFileExists($file);
+        $this->assertContains('RFC 2606', file_get_contents($file));
+        unlink($file);
+    }
+
+    protected function callGetOptionsForUrl($io, array $args = array())
+    {
+        $fs = new RemoteFilesystem($io);
+        $ref = new \ReflectionMethod($fs, 'getOptionsForUrl');
+        $ref->setAccessible(true);
+
+        return $ref->invokeArgs($fs, $args);
+    }
+
+    protected function callCallbackGet(RemoteFilesystem $fs, $notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax)
+    {
+        $ref = new \ReflectionMethod($fs, 'callbackGet');
+        $ref->setAccessible(true);
+        $ref->invoke($fs, $notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax);
+    }
+
+    protected function setAttribute($object, $attribute, $value)
+    {
+        $attr = new \ReflectionProperty($object, $attribute);
+        $attr->setAccessible(true);
+        $attr->setValue($object, $value);
+    }
+}

+ 96 - 0
tests/Composer/Test/Util/StreamContextFactoryTest.php

@@ -0,0 +1,96 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Util;
+
+use Composer\Util\StreamContextFactory;
+
+class StreamContextFactoryTest extends \PHPUnit_Framework_TestCase
+{
+    protected function setUp()
+    {
+        unset($_SERVER['HTTP_PROXY']);
+        unset($_SERVER['http_proxy']);
+    }
+
+    protected function tearDown()
+    {
+        unset($_SERVER['HTTP_PROXY']);
+        unset($_SERVER['http_proxy']);
+    }
+
+    /**
+     * @dataProvider dataGetContext
+     */
+    public function testGetContext($expectedOptions, $defaultOptions, $expectedParams, $defaultParams)
+    {
+        $context = StreamContextFactory::getContext($defaultOptions, $defaultParams);
+        $options = stream_context_get_options($context);
+        $params = stream_context_get_params($context);
+
+        $this->assertEquals($expectedOptions, $options);
+        $this->assertEquals($expectedParams, $params);
+    }
+
+    public function dataGetContext()
+    {
+        return array(
+            array(
+                array(), array(),
+                array('options' => array()), array()
+            ),
+            array(
+                $a = array('http' => array('method' => 'GET')), $a,
+                array('options' => $a, 'notification' => $f = function() {}), array('notification' => $f)
+            ),
+        );
+    }
+
+    public function testHttpProxy()
+    {
+        $_SERVER['HTTP_PROXY'] = 'http://username:password@proxyserver.net:port/';
+        $_SERVER['http_proxy'] = 'http://proxyserver/';
+
+        $context = StreamContextFactory::getContext(array('http' => array('method' => 'GET')));
+        $options = stream_context_get_options($context);
+
+        $this->assertSame('http://proxyserver/', $_SERVER['http_proxy']);
+
+        $this->assertEquals(array('http' => array(
+            'proxy' => 'tcp://username:password@proxyserver.net:port/',
+            'request_fulluri' => true,
+            'method' => 'GET',
+        )), $options);
+    }
+
+    public function testSSLProxy()
+    {
+        $_SERVER['http_proxy'] = 'https://proxyserver/';
+
+        if (extension_loaded('openssl')) {
+            $context = StreamContextFactory::getContext();
+            $options = stream_context_get_options($context);
+
+            $this->assertSame(array('http' => array(
+                'proxy' => 'ssl://proxyserver/',
+                'request_fulluri' => true,
+            )), $options);
+        } else {
+            try {
+                StreamContextFactory::getContext();
+                $this->fail();
+            } catch (\Exception $e) {
+                $this->assertInstanceOf('RuntimeException', $e);
+            }
+        }
+    }
+}