diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 8c8c13bc0456..29e84a9aa64e 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -256,7 +256,7 @@ protected function processUrl(PackageInterface $package, $url) return $url; } - private function getCacheKey(PackageInterface $package, $processedUrl) + public static function getCacheKey(PackageInterface $package, $processedUrl) { // we use the complete download url here to avoid conflicting entries // from different packages, which would potentially allow a given package diff --git a/src/Composer/Downloader/Prefetcher/CopyRequest.php b/src/Composer/Downloader/Prefetcher/CopyRequest.php new file mode 100644 index 000000000000..65ffd059c0a2 --- /dev/null +++ b/src/Composer/Downloader/Prefetcher/CopyRequest.php @@ -0,0 +1,341 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Downloader\Prefetcher; + +use Composer\Util; +use Composer\IO; +use Composer\Config; + +class CopyRequest +{ + private $scheme; + private $user; + private $pass; + private $host; + private $port; + private $path; + private $query = array(); + + /** @var [string => string] */ + private $headers = array(); + + /** @var string */ + private $destination; + + /** @var resource> */ + private $fp; + + private $success = false; + + private static $defaultCurlOptions = array( + CURLOPT_HTTPGET => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 20, + CURLOPT_ENCODING => '', + ); + + private $githubDomains = array(); + private $gitlabDomains = array(); + + private static $NSS_CIPHERS = array( + 'rsa_3des_sha', + 'rsa_des_sha', + 'rsa_null_md5', + 'rsa_null_sha', + 'rsa_rc2_40_md5', + 'rsa_rc4_128_md5', + 'rsa_rc4_128_sha', + 'rsa_rc4_40_md5', + 'fips_des_sha', + 'fips_3des_sha', + 'rsa_des_56_sha', + 'rsa_rc4_56_sha', + 'rsa_aes_128_sha', + 'rsa_aes_256_sha', + 'rsa_aes_128_gcm_sha_256', + 'dhe_rsa_aes_128_gcm_sha_256', + 'ecdh_ecdsa_null_sha', + 'ecdh_ecdsa_rc4_128_sha', + 'ecdh_ecdsa_3des_sha', + 'ecdh_ecdsa_aes_128_sha', + 'ecdh_ecdsa_aes_256_sha', + 'ecdhe_ecdsa_null_sha', + 'ecdhe_ecdsa_rc4_128_sha', + 'ecdhe_ecdsa_3des_sha', + 'ecdhe_ecdsa_aes_128_sha', + 'ecdhe_ecdsa_aes_256_sha', + 'ecdh_rsa_null_sha', + 'ecdh_rsa_128_sha', + 'ecdh_rsa_3des_sha', + 'ecdh_rsa_aes_128_sha', + 'ecdh_rsa_aes_256_sha', + 'echde_rsa_null', + 'ecdhe_rsa_rc4_128_sha', + 'ecdhe_rsa_3des_sha', + 'ecdhe_rsa_aes_128_sha', + 'ecdhe_rsa_aes_256_sha', + 'ecdhe_ecdsa_aes_128_gcm_sha_256', + 'ecdhe_rsa_aes_128_gcm_sha_256', + ); + + /** + * @param string $url + * @param string $destination + * @param bool $useRedirector + * @param IO\IOInterface $io + * @param Config $config + */ + public function __construct($url, $destination, $useRedirector, IO\IOInterface $io, Config $config) + { + $this->setURL($url); + $this->setDestination($destination); + $this->githubDomains = $config->get('github-domains'); + $this->gitlabDomains = $config->get('gitlab-domains'); + $this->setupAuthentication($io, $useRedirector); + } + + public function __destruct() + { + if ($this->fp) { + fclose($this->fp); + } + + if (!$this->success) { + if (file_exists($this->destination)) { + unlink($this->destination); + } + } + } + + /** + * @return string + */ + public function getURL() + { + $url = self::ifOr($this->scheme, '', '://'); + if ($this->user) { + $user = $this->user; + $user .= self::ifOr($this->pass, ':'); + $url .= $user . '@'; + } + $url .= self::ifOr($this->host); + $url .= self::ifOr($this->port, ':'); + $url .= self::ifOr($this->path); + $url .= self::ifOr(http_build_query($this->query), '?'); + return $url; + } + + /** + * @return string user/pass/access_token masked url + */ + public function getMaskedURL() + { + $url = self::ifOr($this->scheme, '', '://'); + $url .= self::ifOr($this->host); + $url .= self::ifOr($this->port, ':'); + $url .= self::ifOr($this->path); + return $url; + } + + private static function ifOr($str, $pre = '', $post = '') + { + if ($str) { + return $pre . $str . $post; + } + return ''; + } + + /** + * @param string $url + */ + public function setURL($url) + { + $struct = parse_url($url); + foreach ($struct as $key => $val) { + if ($key === 'query') { + parse_str($val, $this->query); + } else { + $this->$key = $val; + } + } + } + + public function addParam($key, $val) + { + $this->query[$key] = $val; + } + + public function addHeader($key, $val) + { + $this->headers[strtolower($key)] = $val; + } + + public function makeSuccess() + { + $this->success = true; + } + + /** + * @return array + */ + public function getCurlOptions() + { + $headers = array(); + foreach ($this->headers as $key => $val) { + $headers[] = strtr(ucwords(strtr($key, '-', ' ')), ' ', '-') . ': ' . $val; + } + + $url = $this->getURL(); + + $curlOpts = array( + CURLOPT_URL => $url, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_USERAGENT => Util\StreamContextFactory::generateUserAgent(), + CURLOPT_FILE => $this->fp, + //CURLOPT_VERBOSE => true, //for debug + ); + $curlOpts += self::$defaultCurlOptions; + + if ($ciphers = $this->nssCiphers()) { + $curlOpts[CURLOPT_SSL_CIPHER_LIST] = $ciphers; + } + if ($proxy = $this->getProxy($url)) { + $curlOpts[CURLOPT_PROXY] = $proxy; + } + + return $curlOpts; + } + + /** + * @param IO\IOInterface $io + * @param bool $useRedirector + */ + private function setupAuthentication(IO\IOInterface $io, $useRedirector) + { + if (preg_match('/\.github\.com$/', $this->host)) { + $authKey = 'github.com'; + if ($useRedirector) { + if ($this->host === 'api.github.com' && preg_match('%^/repos(/[^/]+/[^/]+/)zipball(.+)$%', $this->path, $_)) { + $this->host = 'codeload.github.com'; + $this->path = $_[1] . 'legacy.zip' . $_[2]; + } + } + } else { + $authKey = $this->host; + } + if (!$io->hasAuthentication($authKey)) { + if ($this->user || $this->pass) { + $io->setAuthentication($authKey, $this->user, $this->pass); + } else { + return; + } + } + + $auth = $io->getAuthentication($authKey); + + // is github + if (in_array($authKey, $this->githubDomains) && 'x-oauth-basic' === $auth['password']) { + $this->addParam('access_token', $auth['username']); + $this->user = $this->pass = null; + return; + } + // is gitlab + if (in_array($authKey, $this->gitlabDomains) && 'oauth2' === $auth['password']) { + $this->addHeader('authorization', 'Bearer ' . $auth['username']); + $this->user = $this->pass = null; + return; + } + // others, includes bitbucket + $this->user = $auth['username']; + $this->pass = $auth['password']; + } + + private function getProxy($url) + { + if (isset($_SERVER['no_proxy'])) { + $pattern = new Util\NoProxyPattern($_SERVER['no_proxy']); + if ($pattern->test($url)) { + return null; + } + } + + if ($this->scheme === 'https') { + if (isset($_SERVER['HTTPS_PROXY'])) { + return $_SERVER['HTTPS_PROXY']; + } + if (isset($_SERVER['https_proxy'])) { + return $_SERVER['https_proxy']; + } + } + + if ($this->scheme === 'http') { + if (isset($_SERVER['HTTP_PROXY'])) { + return $_SERVER['HTTP_PROXY']; + } + if (isset($_SERVER['http_proxy'])) { + return $_SERVER['http_proxy']; + } + } + return null; + } + + /** + * enable ECC cipher suites in cURL/NSS + */ + public static function nssCiphers() + { + static $cache; + if (isset($cache)) { + return $cache; + } + $ver = curl_version(); + if (preg_match('/^NSS.*Basic ECC$/', $ver['ssl_version'])) { + return $cache = implode(',', self::$NSS_CIPHERS); + } + return $cache = false; + } + + /** + * @param string + */ + public function setDestination($destination) + { + $this->destination = $destination; + if (is_dir($destination)) { + throw new FetchException( + 'The file could not be written to ' . $destination . '. Directory exists.' + ); + } + + $this->createDir($destination); + + $this->fp = fopen($destination, 'wb'); + if (!$this->fp) { + throw new FetchException( + 'The file could not be written to ' . $destination + ); + } + } + + private function createDir($fileName) + { + $targetdir = dirname($fileName); + if (!file_exists($targetdir)) { + if (!mkdir($targetdir, 0766, true)) { + throw new FetchException( + 'The file could not be written to ' . $fileName + ); + } + } + } +} diff --git a/src/Composer/Downloader/Prefetcher/CurlMulti.php b/src/Composer/Downloader/Prefetcher/CurlMulti.php new file mode 100644 index 000000000000..e182ca01d759 --- /dev/null +++ b/src/Composer/Downloader/Prefetcher/CurlMulti.php @@ -0,0 +1,179 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Downloader\Prefetcher; + +class CurlMulti +{ + const MAX_CONNECTIONS = 6; + + /** @var resource */ + private $mh; + + /** @var resource */ + private $sh; + + /** @var resource[] */ + private $unused = array(); + + /** @var resource[] */ + private $using = array(); + + /** @var CopyRequest[] */ + private $requests; + + /** @var CopyRequest[] */ + private $runningRequests; + + /** @var bool */ + private $permanent = true; + + private $blackhole; + + /** + * @param bool $permanent + */ + public function __construct($permanent = true) + { + static $mh_cache, $sh_cache, $ch_cache; + + if (!$permanent || !$mh_cache) { + $mh_cache = curl_multi_init(); + + $ch_cache = array(); + for ($i = 0; $i < self::MAX_CONNECTIONS; ++$i) { + $ch_cache[] = curl_init(); + } + // @codeCoverageIgnoreStart + if (function_exists('curl_share_init')) { + $sh_cache = curl_share_init(); + curl_share_setopt($sh_cache, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); + + foreach ($ch_cache as $ch) { + curl_setopt($ch, CURLOPT_SHARE, $sh_cache); + } + } + // @codeCoverageIgnoreEnd + } + + $this->mh = $mh_cache; + $this->sh = $sh_cache; + $this->unused = $ch_cache; + $this->permanent = $permanent; + + // for PHP<5.5 @see getFinishedResults() + $this->blackhole = fopen('php://temp', 'wb'); + } + + /** + * @codeCoverageIgnore + */ + public function __destruct() + { + foreach ($this->using as $ch) { + curl_multi_remove_handle($this->mh, $ch); + $this->unused[] = $ch; + } + + if ($this->permanent) { + return; //don't close connection + } + + foreach ($this->unused as $ch) { + curl_close($ch); + } + + curl_multi_close($this->mh); + } + + /** + * @param CopyRequest[] $requests + */ + public function setRequests(array $requests) + { + $this->requests = $requests; + } + + public function setupEventLoop() + { + while (count($this->unused) > 0 && count($this->requests) > 0) { + $request = array_pop($this->requests); + $ch = array_pop($this->unused); + $index = (int)$ch; + + $this->using[$index] = $ch; + $this->runningRequests[$index] = $request; + + curl_setopt_array($ch, $request->getCurlOptions()); + curl_multi_add_handle($this->mh, $ch); + } + } + + public function wait() + { + $expectRunning = count($this->using); + $running = 0; + $retryCnt = 0; + + do { + do { + $stat = curl_multi_exec($this->mh, $running); + } while ($stat === CURLM_CALL_MULTI_PERFORM); + if (-1 === curl_multi_select($this->mh)) { + // @codeCoverageIgnoreStart + if ($retryCnt++ > 100) { + throw new FetchException('curl_multi_select failure'); + } + // @codeCoverageIgnoreEnd + usleep(100000); + } + } while ($running > 0 && $running >= $expectRunning); + } + + public function getFinishedResults() + { + $urls = array(); + $successCnt = $failureCnt = 0; + do { + if ($raised = curl_multi_info_read($this->mh, $remains)) { + $ch = $raised['handle']; + $errno = curl_errno($ch); + $error = curl_error($ch); + $info = curl_getinfo($ch); + curl_setopt($ch, CURLOPT_FILE, $this->blackhole); //release file pointer + $index = (int)$ch; + $request = $this->runningRequests[$index]; + if (CURLE_OK === $errno && !$error && ('http' !== substr($info['url'], 0, 4) || 200 === $info['http_code'])) { + ++$successCnt; + $request->makeSuccess(); + $urls[] = $request->getMaskedURL(); + } else { + ++$failureCnt; + } + unset($this->using[$index], $this->runningRequests[$index], $request); + curl_multi_remove_handle($this->mh, $ch); + $this->unused[] = $ch; + } + } while ($remains > 0); + + return array( + 'successCnt' => $successCnt, + 'failureCnt' => $failureCnt, + 'urls' => $urls, + ); + } + + public function remain() + { + return count($this->runningRequests) > 0; + } +} diff --git a/src/Composer/Downloader/Prefetcher/FetchException.php b/src/Composer/Downloader/Prefetcher/FetchException.php new file mode 100644 index 000000000000..4075bead935b --- /dev/null +++ b/src/Composer/Downloader/Prefetcher/FetchException.php @@ -0,0 +1,17 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Downloader\Prefetcher; + +class FetchException extends \RuntimeException +{ +} diff --git a/src/Composer/Downloader/Prefetcher/Prefetcher.php b/src/Composer/Downloader/Prefetcher/Prefetcher.php new file mode 100644 index 000000000000..f6cbd6e201e9 --- /dev/null +++ b/src/Composer/Downloader/Prefetcher/Prefetcher.php @@ -0,0 +1,113 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Downloader\Prefetcher; + +use Composer\Downloader\FileDownloader; +use Composer\IO; +use Composer\Config; +use Composer\Package; +use Composer\DependencyResolver\Operation; + +class Prefetcher +{ + /** + * @param IO\IOInterface $io + * @param CopyRequest[] $requests + */ + public function fetchAll(IO\IOInterface $io, array $requests) + { + $successCnt = $failureCnt = 0; + $totalCnt = count($requests); + + $multi = new CurlMulti; + $multi->setRequests($requests); + try { + do { + $multi->setupEventLoop(); + $multi->wait(); + + $result = $multi->getFinishedResults(); + $successCnt += $result['successCnt']; + $failureCnt += $result['failureCnt']; + foreach ($result['urls'] as $url) { + $io->writeError(" $successCnt/$totalCnt:\t$url"); + } + } while ($multi->remain()); + } catch (FetchException $e) { + // do nothing + } + + $skippedCnt = $totalCnt - $successCnt - $failureCnt; + $io->writeError(" Finished: success:$successCnt, skipped:$skippedCnt, failure:$failureCnt, total: $totalCnt"); + } + + /** + * @param IO\IOInterface $io + * @param Config $config + * @param Operation\OperationInterface[] $ops + */ + public function fetchAllFromOperations(IO\IOInterface $io, Config $config, array $ops) + { + $cachedir = rtrim($config->get('cache-files-dir'), '\/'); + $requests = array(); + foreach ($ops as $op) { + switch ($op->getJobType()) { + case 'install': + $p = $op->getPackage(); + break; + case 'update': + $p = $op->getTargetPackage(); + break; + default: + continue 2; + } + + $url = $this->getUrlFromPackage($p); + if (!$url) { + continue; + } + + $destination = $cachedir . DIRECTORY_SEPARATOR . FileDownloader::getCacheKey($p, $url); + if (file_exists($destination)) { + continue; + } + $useRedirector = (bool)preg_match('%^(?:https|git)://github\.com%', $p->getSourceUrl()); + try { + $request = new CopyRequest($url, $destination, $useRedirector, $io, $config); + $requests[] = $request; + } catch (FetchException $e) { + // do nothing + } + } + + if (count($requests) > 0) { + $this->fetchAll($io, $requests); + } + } + + private static function getUrlFromPackage(Package\PackageInterface $package) + { + $url = $package->getDistUrl(); + if (!$url) { + return false; + } + if ($package->getDistMirrors()) { + $url = current($package->getDistUrls()); + } + if (!parse_url($url, PHP_URL_HOST)) { + return false; + } + return $url; + } + +} diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index ec815745605e..9231d98a2be3 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -25,6 +25,7 @@ use Composer\DependencyResolver\Solver; use Composer\DependencyResolver\SolverProblemsException; use Composer\Downloader\DownloadManager; +use Composer\Downloader\Prefetcher; use Composer\EventDispatcher\EventDispatcher; use Composer\Installer\InstallationManager; use Composer\Installer\InstallerEvents; @@ -504,6 +505,11 @@ protected function doInstall($localRepo, $installedRepo, $platformRepo, $aliases $devPackages = null; } + if (extension_loaded('curl')) { + $prefetcher = new Prefetcher\Prefetcher; + $prefetcher->fetchAllFromOperations($this->io, $this->config, $operations); + } + if ($operations) { $installs = $updates = $uninstalls = array(); foreach ($operations as $operation) { diff --git a/src/Composer/Util/StreamContextFactory.php b/src/Composer/Util/StreamContextFactory.php index 9fd28388dacb..5f6f85f19986 100644 --- a/src/Composer/Util/StreamContextFactory.php +++ b/src/Composer/Util/StreamContextFactory.php @@ -133,24 +133,29 @@ public static function getContext($url, array $defaultOptions = array(), array $ $options['http']['header'] = self::fixHttpHeaderField($options['http']['header']); } + if (!isset($options['http']['header']) || false === strpos(strtolower(implode('', $options['http']['header'])), 'user-agent')) { + $options['http']['header'][] = 'User-Agent: ' . self::generateUserAgent(); + } + + return stream_context_create($options, $defaultParams); + } + + public static function generateUserAgent() + { if (defined('HHVM_VERSION')) { $phpVersion = 'HHVM ' . HHVM_VERSION; } else { $phpVersion = 'PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION; } - if (!isset($options['http']['header']) || false === strpos(strtolower(implode('', $options['http']['header'])), 'user-agent')) { - $options['http']['header'][] = sprintf( - 'User-Agent: Composer/%s (%s; %s; %s%s)', - Composer::VERSION === '@package_version@' ? 'source' : Composer::VERSION, - php_uname('s'), - php_uname('r'), - $phpVersion, - getenv('CI') ? '; CI' : '' - ); - } - - return stream_context_create($options, $defaultParams); + return sprintf( + 'Composer/%s (%s; %s; %s%s)', + Composer::VERSION === '@package_version@' ? 'source' : Composer::VERSION, + php_uname('s'), + php_uname('r'), + $phpVersion, + getenv('CI') ? '; CI' : '' + ); } /** diff --git a/tests/Composer/Test/Downloader/Prefetcher/CopyRequestTest.php b/tests/Composer/Test/Downloader/Prefetcher/CopyRequestTest.php new file mode 100644 index 000000000000..23d5a2e1875a --- /dev/null +++ b/tests/Composer/Test/Downloader/Prefetcher/CopyRequestTest.php @@ -0,0 +1,136 @@ +iop = $this->prophesize('Composer\IO\IOInterface'); + $this->configp = $configp = $this->prophesize('Composer\Config'); + $configp->get('github-domains')->willReturn(array('github.com')); + $configp->get('gitlab-domains')->willReturn(array('gitlab.com')); + } + + public function testConstruct() + { + $tmpfile = tempnam(sys_get_temp_dir(), 'composer_unit_test_'); + $example = 'http://user:pass@example.com:80/p/a/t/h?a=b'; + + $this->iop->setAuthentication('example.com', 'user', 'pass') + ->will(function($args, $iop){ + $iop->getAuthentication($args[0]) + ->willReturn(array('username' => $args[1], 'password' => $args[2])); + }) + ->shouldBeCalled(); + $this->iop->hasAuthentication(arg::type('string')) + ->willReturn(false); + + $req = new CopyRequest($example, $tmpfile, false, $this->iop->reveal(), $this->configp->reveal()); + $this->assertEquals($example, $req->getURL()); + } + + public function testDirectoryExists() + { + $rand = sha1(mt_rand()); + $tmpbase = sys_get_temp_dir(); + $tmpdir = $tmpbase . DIRECTORY_SEPARATOR . $rand; + mkdir($tmpdir); + + try { + // if is_dir(destination) then throws exception + $req = new CopyRequest('http://example.com/', $tmpdir, false, $this->iop->reveal(), $this->configp->reveal()); + rmdir($tmpdir); + $this->fail('expectedException: \Composer\Downloader\Prefetcher\FetchException'); + } catch (\Exception $e) { + rmdir($tmpdir); + $this->assertInstanceOf('Composer\Downloader\Prefetcher\FetchException', $e); + $this->assertContains('Directory exists', $e->getMessage()); + } + } + + public function testDestruct() + { + $tmpfile = tempnam(sys_get_temp_dir(), 'composer_unit_test_'); + + $req = new CopyRequest('http://example.com/', $tmpfile, false, $this->iop->reveal(), $this->configp->reveal()); + $this->assertFileExists($tmpfile); + + // if $req->success === true ... + $req->makeSuccess(); + unset($req); + + // then tmpfile remain + $this->assertFileExists($tmpfile); + unlink($tmpfile); + + $req = new CopyRequest('http://example.com/', $tmpfile, false, $this->iop->reveal(), $this->configp->reveal()); + // if $req->success === false (default) ... + // $req->makeSuccess(); + unset($req); + + // then cleaned tmpfile automatically + $this->assertFileNotExists($tmpfile); + } + + public function testGetMaskedURL() + { + $tmpfile = tempnam(sys_get_temp_dir(), 'composer_unit_test_'); + + $req = new CopyRequest('http://user:pass@example.com/p/a/t/h?token=opensesame', $tmpfile, false, $this->iop->reveal(), $this->configp->reveal()); + // user/pass/query masked + $this->assertEquals('http://example.com/p/a/t/h', $req->getMaskedURL()); + } + + public function testGitHubRedirector() + { + $tmpfile = tempnam(sys_get_temp_dir(), 'composer_unit_test_'); + $example = 'https://api.github.com/repos/vendor/name/zipball/aaaa?a=b'; + + $this->iop->hasAuthentication('github.com')->willReturn(true); + $this->iop->getAuthentication('github.com')->willReturn(array('username' => 'at', 'password' => 'x-oauth-basic')); + + // user:pass -> query + $req = new CopyRequest($example, $tmpfile, false, $this->iop->reveal(), $this->configp->reveal()); + $this->assertEquals("$example&access_token=at", $req->getURL()); + + // api.github.com -> codeload.github.com + $req = new CopyRequest($example, $tmpfile, true, $this->iop->reveal(), $this->configp->reveal()); + $this->assertEquals('https://codeload.github.com/vendor/name/legacy.zip/aaaa?a=b&access_token=at', $req->getURL()); + } + + public function testGitLab() + { + $tmpfile = tempnam(sys_get_temp_dir(), 'composer_unit_test_'); + $example = 'https://gitlab.com/p/a/t/h'; + + $this->iop->hasAuthentication('gitlab.com')->willReturn(true); + $this->iop->getAuthentication('gitlab.com')->willReturn(array('username' => 'at', 'password' => 'oauth2')); + + $req = new CopyRequest($example, $tmpfile, false, $this->iop->reveal(), $this->configp->reveal()); + $opts = $req->getCurlOptions(); + $this->assertContains('Authorization: Bearer at', $opts[CURLOPT_HTTPHEADER]); + } + + public function testProxy() + { + $serverBackup = $_SERVER; + + $tmpfile = tempnam(sys_get_temp_dir(), 'composer_unit_test_'); + + $_SERVER['no_proxy'] = 'example.com'; + $_SERVER['HTTP_PROXY'] = 'http://example.com:8080'; + $req = new CopyRequest('http://localhost', $tmpfile, false, $this->iop->reveal(), $this->configp->reveal()); + $this->assertArrayHasKey(CURLOPT_PROXY, $req->getCurlOptions()); + + $req = new CopyRequest('http://example.com', $tmpfile, false, $this->iop->reveal(), $this->configp->reveal()); + $this->assertArrayNotHasKey(CURLOPT_PROXY, $req->getCurlOptions()); + + $_SERVER = $serverBackup; + } +} diff --git a/tests/Composer/Test/Downloader/Prefetcher/CurlMultiTest.php b/tests/Composer/Test/Downloader/Prefetcher/CurlMultiTest.php new file mode 100644 index 000000000000..dacdaa143539 --- /dev/null +++ b/tests/Composer/Test/Downloader/Prefetcher/CurlMultiTest.php @@ -0,0 +1,76 @@ +iop = $this->prophesize('Composer\IO\IOInterface'); + $this->configp = $configp = $this->prophesize('Composer\Config'); + $configp->get('github-domains')->willReturn(array('github.com')); + $configp->get('gitlab-domains')->willReturn(array('gitlab.com')); + } + + public function testRequestSuccess() + { + $tmpfile = tmpfile(); + $reqp = $this->prophesize('Composer\Downloader\Prefetcher\CopyRequest'); + $reqp->getCurlOptions()->willReturn(array( + CURLOPT_URL => 'file://' . __DIR__ . '/test.txt', + CURLOPT_FILE => $tmpfile, + )); + $reqp->getMaskedURL()->willReturn('file://' . __DIR__ . '/test.txt'); + $reqp->makeSuccess()->willReturn(null); + $requests = array($reqp->reveal()); + + $multi = new CurlMulti; + $multi->setRequests($requests); + + do { + $multi->setupEventLoop(); + $multi->wait(); + + $result = $multi->getFinishedResults(); + $this->assertEquals(1, $result['successCnt']); + $this->assertEquals(0, $result['failureCnt']); + } while ($multi->remain()); + + rewind($tmpfile); + $content = stream_get_contents($tmpfile); + $this->assertEquals(file_get_contents(__DIR__ . '/test.txt'), $content); + } + + public function testWait() + { + $tmpfile = tmpfile(); + $reqp = $this->prophesize('Composer\Downloader\Prefetcher\CopyRequest'); + $reqp->getCurlOptions()->willReturn(array( + CURLOPT_URL => 'file://uso800.txt', + CURLOPT_FILE => $tmpfile, + )); + $reqp->getMaskedURL()->willReturn('file://uso800.txt'); + $reqp->makeSuccess()->willReturn(null); + $requests = array($reqp->reveal()); + + $multi = new CurlMulti; + $multi->setRequests($requests); + + do { + $multi->setupEventLoop(); + $multi->wait(); + + $result = $multi->getFinishedResults(); + $this->assertEquals(0, $result['successCnt']); + $this->assertEquals(1, $result['failureCnt']); + } while ($multi->remain()); + + rewind($tmpfile); + $content = stream_get_contents($tmpfile); + $this->assertEmpty($content); + } +} diff --git a/tests/Composer/Test/Downloader/Prefetcher/PrefetcherTest.php b/tests/Composer/Test/Downloader/Prefetcher/PrefetcherTest.php new file mode 100644 index 000000000000..17560afc8a7a --- /dev/null +++ b/tests/Composer/Test/Downloader/Prefetcher/PrefetcherTest.php @@ -0,0 +1,103 @@ +iop = $this->prophesize('Composer\IO\IOInterface'); + $this->configp = $configp = $this->prophesize('Composer\Config'); + $configp->get('github-domains')->willReturn(array('github.com')); + $configp->get('gitlab-domains')->willReturn(array('gitlab.com')); + $configp->get('cache-files-dir')->willReturn(sys_get_temp_dir()); + } + + public function testFetchAllOnFailure() + { + $reqp = $this->prophesize('Composer\Downloader\Prefetcher\CopyRequest'); + $reqp->getCurlOptions()->willReturn(array( + CURLOPT_URL => 'file://uso800.txt', + CURLOPT_FILE => tmpfile(), + )); + $this->iop->writeError(" Finished: success:0, skipped:0, failure:1, total: 1")->shouldBeCalledTimes(1); + + $fetcher = new Prefetcher; + $fetcher->fetchAll($this->iop->reveal(), array($reqp->reveal())); + } + + public function testFetchAllOnSuccess() + { + $reqp = $this->prophesize('Composer\Downloader\Prefetcher\CopyRequest'); + $reqp->getCurlOptions()->willReturn(array( + CURLOPT_URL => 'file://' . __DIR__ . '/test.txt', + CURLOPT_FILE => tmpfile(), + )); + $reqp->makeSuccess()->willReturn(null); + $reqp->getMaskedURL()->willReturn('file://' . __DIR__ . '/test.txt'); + $this->iop->writeError(arg::type('string'))->shouldBeCalled(); + + $fetcher = new Prefetcher; + $fetcher->fetchAll($this->iop->reveal(), array($reqp->reveal())); + } + + public function testFetchAllFromOperationsWithNoOperations() + { + $opp = $this->prophesize('Composer\DependencyResolver\Operation\OperationInterface'); + $opp->getJobType()->willReturn('remove'); + + $this->iop->writeError(arg::any())->shouldNotBeCalled(); + + $fetcher = new Prefetcher; + $fetcher->fetchAllFromOperations($this->iop->reveal(), $this->configp->reveal(), array($opp->reveal())); + } + + private function createProphecies() + { + $opp = $this->prophesize('Composer\DependencyResolver\Operation\InstallOperation'); + $opp->getJobType()->willReturn('install'); + $pp = $this->prophesize('Composer\Package\PackageInterface'); + return array($opp, $pp); + } + + public function testFetchAllWithInstallOperation() + { + list($opp, $pp) = $this->createProphecies(); + $pp->getName()->willReturn('acme/acme'); + $pp->getDistType()->willReturn('composer'); + $pp->getDistUrl()->willReturn('file://' . __DIR__ . '/test.txt'); + $pp->getDistMirrors()->willReturn(array()); + $pp->getSourceUrl()->shouldNotBeCalled(); + + $opp->getPackage()->willReturn($pp->reveal())->shouldBeCalled(); + + $fetcher = new Prefetcher; + $fetcher->fetchAllFromOperations($this->iop->reveal(), $this->configp->reveal(), array($opp->reveal())); + } + + public function testFetchAllWithInstallButFileExists() + { + list($opp, $pp) = $this->createProphecies(); + $pp->getName()->willReturn(''); + $pp->getDistType()->willReturn('html'); + $pp->getDistUrl()->willReturn('http://example.com/'); + $pp->getDistMirrors()->willReturn(array()); + $pp->getSourceUrl()->willReturn('git://uso800'); + + $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . FileDownloader::getCacheKey($pp->reveal(), 'http://example.com/'); + $fp = fopen($path, 'wb'); + + $opp->getPackage()->willReturn($pp->reveal())->shouldBeCalled(); + + $fetcher = new Prefetcher; + $fetcher->fetchAllFromOperations($this->iop->reveal(), $this->configp->reveal(), array($opp->reveal())); + fclose($fp); + unlink($path); + } +} diff --git a/tests/Composer/Test/Downloader/Prefetcher/test.txt b/tests/Composer/Test/Downloader/Prefetcher/test.txt new file mode 100644 index 000000000000..6684d285884e --- /dev/null +++ b/tests/Composer/Test/Downloader/Prefetcher/test.txt @@ -0,0 +1 @@ +Composer\Downloader\Prefetcher test