diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php new file mode 100644 index 000000000..07d418bed --- /dev/null +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -0,0 +1,99 @@ + + * Jordi Boggiano + * + * 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 + * @author Jordi Boggiano + * @author François Pluchino + */ +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); +} diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 6a4d68103..698ebfd46 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/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 * @author Jordi Boggiano * @author François Pluchino */ -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 " . $package->getName() . " (" . $package->getPrettyVersion() . ")"); - 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; + } } diff --git a/src/Composer/Downloader/PharDownloader.php b/src/Composer/Downloader/PharDownloader.php index 83a38a4a3..d7b1fae46 100644 --- a/src/Composer/Downloader/PharDownloader.php +++ b/src/Composer/Downloader/PharDownloader.php @@ -19,7 +19,7 @@ use Composer\Package\PackageInterface; * * @author Kirill chEbba Chebunin */ -class PharDownloader extends FileDownloader +class PharDownloader extends ArchiveDownloader { /** * {@inheritDoc} diff --git a/src/Composer/Downloader/TarDownloader.php b/src/Composer/Downloader/TarDownloader.php index c239475f7..0429a4232 100644 --- a/src/Composer/Downloader/TarDownloader.php +++ b/src/Composer/Downloader/TarDownloader.php @@ -19,7 +19,7 @@ use Composer\Package\PackageInterface; * * @author Kirill chEbba Chebunin */ -class TarDownloader extends FileDownloader +class TarDownloader extends ArchiveDownloader { /** * {@inheritDoc} diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index 7ae23e04e..3618fc07b 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -19,7 +19,7 @@ use Composer\IO\IOInterface; /** * @author Jordi Boggiano */ -class ZipDownloader extends FileDownloader +class ZipDownloader extends ArchiveDownloader { protected $process; diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index cf70c4c74..44b97fcf2 100644 --- a/src/Composer/Factory.php +++ b/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; } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 89325720f..7bd0f3bfe 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/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; } } -} \ No newline at end of file + + 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; + } +} diff --git a/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php new file mode 100644 index 000000000..fbbf2b269 --- /dev/null +++ b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php @@ -0,0 +1,35 @@ + + * Jordi Boggiano + * + * 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')); + } +} diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php new file mode 100644 index 000000000..cc0fb2f6a --- /dev/null +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -0,0 +1,145 @@ + + * Jordi Boggiano + * + * 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()); + } + } +} diff --git a/tests/Composer/Test/Downloader/Util/FilesystemTest.php b/tests/Composer/Test/Util/FilesystemTest.php similarity index 91% rename from tests/Composer/Test/Downloader/Util/FilesystemTest.php rename to tests/Composer/Test/Util/FilesystemTest.php index 9605a111c..0db5549d2 100644 --- a/tests/Composer/Test/Downloader/Util/FilesystemTest.php +++ b/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"), diff --git a/tests/Composer/Test/Util/RemoteFilesystemTest.php b/tests/Composer/Test/Util/RemoteFilesystemTest.php new file mode 100644 index 000000000..c7e5c076e --- /dev/null +++ b/tests/Composer/Test/Util/RemoteFilesystemTest.php @@ -0,0 +1,186 @@ + + * 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\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); + } +} diff --git a/tests/Composer/Test/Util/StreamContextFactoryTest.php b/tests/Composer/Test/Util/StreamContextFactoryTest.php new file mode 100644 index 000000000..eab6e7936 --- /dev/null +++ b/tests/Composer/Test/Util/StreamContextFactoryTest.php @@ -0,0 +1,96 @@ + + * 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\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); + } + } + } +}