diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index b2d55bb4f..508a76afa 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -12,10 +12,12 @@ namespace Composer\Downloader; +use Composer\Config; use Composer\IO\IOInterface; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; use Composer\Util\Filesystem; +use Composer\Util\GitHub; use Composer\Util\RemoteFilesystem; /** @@ -28,6 +30,7 @@ use Composer\Util\RemoteFilesystem; class FileDownloader implements DownloaderInterface { protected $io; + protected $config; protected $rfs; protected $filesystem; @@ -36,9 +39,10 @@ class FileDownloader implements DownloaderInterface * * @param IOInterface $io The IO instance */ - public function __construct(IOInterface $io, RemoteFilesystem $rfs = null, Filesystem $filesystem = null) + public function __construct(IOInterface $io, Config $config, RemoteFilesystem $rfs = null, Filesystem $filesystem = null) { $this->io = $io; + $this->config = $config; $this->rfs = $rfs ?: new RemoteFilesystem($io); $this->filesystem = $filesystem ?: new Filesystem(); } @@ -70,7 +74,18 @@ class FileDownloader implements DownloaderInterface $processUrl = $this->processUrl($package, $url); try { - $this->rfs->copy(parse_url($processUrl, PHP_URL_HOST), $processUrl, $fileName); + try { + $this->rfs->copy(parse_url($processUrl, PHP_URL_HOST), $processUrl, $fileName); + } catch (TransportException $e) { + if (404 === $e->getCode() && 'github.com' === parse_url($processUrl, PHP_URL_HOST)) { + $message = "\n".'Could not fetch '.$processUrl.', enter your GitHub credentials to access private repos'; + $gitHubUtil = new GitHub($this->io, $this->config, null, $this->rfs); + $gitHubUtil->authorizeOAuth('github.com', $message); + $this->rfs->copy(parse_url($processUrl, PHP_URL_HOST), $processUrl, $fileName); + } else { + throw $e; + } + } if (!file_exists($fileName)) { throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the' diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 52e269024..c80b57634 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -13,6 +13,7 @@ namespace Composer\Downloader; use Composer\Package\PackageInterface; +use Composer\Util\GitHub; /** * @author Jordi Boggiano @@ -283,32 +284,21 @@ class GitDownloader extends VcsDownloader $command = call_user_func($commandCallable, $url); if (0 !== $this->process->execute($command, $handler)) { - if (preg_match('{^git@github.com:(.+?)\.git$}i', $url, $match) && $this->io->isInteractive()) { - // private github repository without git access, try https with auth - $retries = 3; - $retrying = false; - do { - if ($retrying) { - $this->io->write('Invalid credentials'); - } - if (!$this->io->hasAuthorization('github.com') || $retrying) { - $username = $this->io->ask('Username: '); - $password = $this->io->askAndHideAnswer('Password: '); - $this->io->setAuthorization('github.com', $username, $password); - } + // private github repository without git access, try https with auth + if (preg_match('{^git@(github.com):(.+?)\.git$}i', $url, $match) && $this->io->isInteractive()) { + if (!$this->io->hasAuthorization($match[1])) { + $message = 'Cloning failed using an ssh key for authentication, enter your GitHub credentials to access private repos'; + $gitHubUtil = new GitHub($this->io, $this->config, $this->process); + $gitHubUtil->authorizeOAuth($match[1], $message); + } - $auth = $this->io->getAuthorization('github.com'); - $url = 'https://'.$auth['username'] . ':' . $auth['password'] . '@github.com/'.$match[1].'.git'; + $auth = $this->io->getAuthorization($match[1]); + $url = 'https://'.$auth['username'] . ':' . $auth['password'] . '@'.$match[1].'/'.$match[2].'.git'; - $command = call_user_func($commandCallable, $url); - if (0 === $this->process->execute($command, $handler)) { - return; - } - if (null !== $path) { - $this->filesystem->removeDirectory($path); - } - $retrying = true; - } while (--$retries); + $command = call_user_func($commandCallable, $url); + if (0 === $this->process->execute($command, $handler)) { + return; + } } if (null !== $path) { diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index 79f636b55..dff84c504 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -12,6 +12,7 @@ namespace Composer\Downloader; +use Composer\Config; use Composer\Util\ProcessExecutor; use Composer\IO\IOInterface; use ZipArchive; @@ -23,10 +24,10 @@ class ZipDownloader extends ArchiveDownloader { protected $process; - public function __construct(IOInterface $io, ProcessExecutor $process = null) + public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null) { $this->process = $process ?: new ProcessExecutor; - parent::__construct($io); + parent::__construct($io, $config); } protected function extract($file, $path) diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 4862ff995..c23fa710c 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -234,10 +234,10 @@ class Factory $dm->setDownloader('git', new Downloader\GitDownloader($io, $config)); $dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config)); $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config)); - $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)); + $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config)); + $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config)); + $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config)); + $dm->setDownloader('file', new Downloader\FileDownloader($io, $config)); return $dm; } diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index df8777d05..7c53970cd 100755 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -17,6 +17,7 @@ use Composer\Json\JsonFile; use Composer\Cache; use Composer\IO\IOInterface; use Composer\Util\RemoteFilesystem; +use Composer\Util\GitHub; /** * @author Jordi Boggiano @@ -255,8 +256,7 @@ class GitHubDriver extends VcsDriver return $this->attemptCloneFallback($e); } - $this->io->write('Your GitHub credentials are required to fetch private repository metadata ('.$this->url.'):'); - $this->authorizeOauth(); + $this->authorizeOAuth('Your GitHub credentials are required to fetch private repository metadata ('.$this->url.')'); return parent::getContents($url); @@ -278,8 +278,7 @@ class GitHubDriver extends VcsDriver throw $e; } - $this->io->write('API limit exhausted. Enter your GitHub credentials to get a larger API limit ('.$this->url.'):'); - $this->authorizeOauth(); + $this->authorizeOAuth('API limit exhausted. Enter your GitHub credentials to get a larger API limit ('.$this->url.')'); return parent::getContents($url); } @@ -348,61 +347,9 @@ class GitHubDriver extends VcsDriver } } - protected function authorizeOAuth() + protected function authorizeOAuth($message) { - // If available use token from git config - if (0 === $this->process->execute('git config github.accesstoken', $output)) { - $this->io->write('Using Github OAuth token stored in git config (github.accesstoken)'); - $this->io->setAuthorization($this->originUrl, $output[0], 'x-oauth-basic'); - return; - } - - $attemptCounter = 0; - - $this->io->write('The credentials will be swapped for an OAuth token stored in '.$this->config->get('home').'/config.json, your password will not be stored'); - $this->io->write('To revoke access to this token you can visit https://github.com/settings/applications'); - while ($attemptCounter++ < 5) { - try { - $username = $this->io->ask('Username: '); - $password = $this->io->askAndHideAnswer('Password: '); - $this->io->setAuthorization($this->originUrl, $username, $password); - - // build up OAuth app name - $appName = 'Composer'; - if (0 === $this->process->execute('hostname', $output)) { - $appName .= ' on ' . trim($output); - } - - $contents = JsonFile::parseJson($this->remoteFilesystem->getContents($this->originUrl, 'https://api.github.com/authorizations', false, array( - 'http' => array( - 'method' => 'POST', - 'header' => "Content-Type: application/json\r\n", - 'content' => json_encode(array( - 'scopes' => array('repo'), - 'note' => $appName, - 'note_url' => 'https://getcomposer.org/', - )), - ) - ))); - } catch (TransportException $e) { - if (401 === $e->getCode()) { - $this->io->write('Invalid credentials.'); - continue; - } - - throw $e; - } - - $this->io->setAuthorization($this->originUrl, $contents['token'], 'x-oauth-basic'); - - // store value in user config - $githubTokens = $this->config->get('github-oauth') ?: array(); - $githubTokens[$this->originUrl] = $contents['token']; - $this->config->getConfigSource()->addConfigSetting('github-oauth', $githubTokens); - - return; - } - - throw new \RuntimeException("Invalid GitHub credentials 5 times in a row, aborting."); + $gitHubUtil = new GitHub($this->io, $this->config, $this->process, $this->remoteFilesystem); + $gitHubUtil->authorizeOAuth($this->originUrl, $message); } } diff --git a/src/Composer/Repository/Vcs/VcsDriver.php b/src/Composer/Repository/Vcs/VcsDriver.php index c7a3be24d..16af91946 100644 --- a/src/Composer/Repository/Vcs/VcsDriver.php +++ b/src/Composer/Repository/Vcs/VcsDriver.php @@ -36,13 +36,13 @@ abstract class VcsDriver implements VcsDriverInterface /** * Constructor. * - * @param array $repoConfig The repository configuration - * @param IOInterface $io The IO instance - * @param Config $config The composer configuration - * @param ProcessExecutor $process Process instance, injectable for mocking - * @param callable $remoteFilesystem Remote Filesystem, injectable for mocking + * @param array $repoConfig The repository configuration + * @param IOInterface $io The IO instance + * @param Config $config The composer configuration + * @param ProcessExecutor $process Process instance, injectable for mocking + * @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking */ - final public function __construct(array $repoConfig, IOInterface $io, Config $config, ProcessExecutor $process = null, $remoteFilesystem = null) + final public function __construct(array $repoConfig, IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null) { $this->url = $repoConfig['url']; $this->originUrl = $repoConfig['url']; diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php new file mode 100644 index 000000000..9cb4b160c --- /dev/null +++ b/src/Composer/Util/GitHub.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\Util; + +use Composer\IO\IOInterface; +use Composer\Config; +use Composer\Downloader\TransportException; +use Composer\Json\JsonFile; + +/** + * @author Jordi Boggiano + */ +class GitHub +{ + protected $io; + protected $config; + protected $process; + protected $remoteFilesystem; + + /** + * Constructor. + * + * @param IOInterface $io The IO instance + * @param Config $config The composer configuration + * @param ProcessExecutor $process Process instance, injectable for mocking + * @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking + */ + public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null) + { + $this->io = $io; + $this->config = $config; + $this->process = $process ?: new ProcessExecutor; + $this->remoteFilesystem = $remoteFilesystem ?: new RemoteFilesystem($io); + } + + /** + * Authorizes a GitHub domain via OAuth + * + * @param string $originUrl The host this GitHub instance is located at + * @param string $message The reason this authorization is required + */ + public function authorizeOAuth($originUrl, $message = null) + { + // if available use token from git config + if (0 === $this->process->execute('git config github.accesstoken', $output)) { + $this->io->write('Using Github OAuth token stored in git config (github.accesstoken)'); + $this->io->setAuthorization($originUrl, trim($output), 'x-oauth-basic'); + + return; + } + + $attemptCounter = 0; + + if ($message) { + $this->io->write($message); + } + $this->io->write('The credentials will be swapped for an OAuth token stored in '.$this->config->get('home').'/config.json, your password will not be stored'); + $this->io->write('To revoke access to this token you can visit https://github.com/settings/applications'); + while ($attemptCounter++ < 5) { + try { + $username = $this->io->ask('Username: '); + $password = $this->io->askAndHideAnswer('Password: '); + $this->io->setAuthorization($originUrl, $username, $password); + + // build up OAuth app name + $appName = 'Composer'; + if (0 === $this->process->execute('hostname', $output)) { + $appName .= ' on ' . trim($output); + } + + $contents = JsonFile::parseJson($this->remoteFilesystem->getContents($originUrl, 'https://api.github.com/authorizations', false, array( + 'http' => array( + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\n", + 'content' => json_encode(array( + 'scopes' => array('repo'), + 'note' => $appName, + 'note_url' => 'https://getcomposer.org/', + )), + ) + ))); + } catch (TransportException $e) { + if (in_array($e->getCode(), array(403, 401))) { + $this->io->write('Invalid credentials.'); + continue; + } + + throw $e; + } + + $this->io->setAuthorization($originUrl, $contents['token'], 'x-oauth-basic'); + + // store value in user config + $githubTokens = $this->config->get('github-oauth') ?: array(); + $githubTokens[$originUrl] = $contents['token']; + $this->config->getConfigSource()->addConfigSetting('github-oauth', $githubTokens); + + return; + } + + throw new \RuntimeException("Invalid GitHub credentials 5 times in a row, aborting."); + } +} diff --git a/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php index 72ba9b10a..eb8709706 100644 --- a/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php @@ -22,7 +22,7 @@ class ArchiveDownloaderTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue('http://example.com/script.js')) ; - $downloader = $this->getMockForAbstractClass('Composer\Downloader\ArchiveDownloader', array($this->getMock('Composer\IO\IOInterface'))); + $downloader = $this->getMockForAbstractClass('Composer\Downloader\ArchiveDownloader', array($this->getMock('Composer\IO\IOInterface'), $this->getMock('Composer\Config'))); $method = new \ReflectionMethod($downloader, 'getFileName'); $method->setAccessible(true); @@ -33,7 +33,7 @@ class ArchiveDownloaderTest extends \PHPUnit_Framework_TestCase public function testProcessUrl() { - $downloader = $this->getMockForAbstractClass('Composer\Downloader\ArchiveDownloader', array($this->getMock('Composer\IO\IOInterface'))); + $downloader = $this->getMockForAbstractClass('Composer\Downloader\ArchiveDownloader', array($this->getMock('Composer\IO\IOInterface'), $this->getMock('Composer\Config'))); $method = new \ReflectionMethod($downloader, 'processUrl'); $method->setAccessible(true); diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php index a6fff5f1b..e68fe32f1 100644 --- a/tests/Composer/Test/Downloader/FileDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -17,12 +17,13 @@ use Composer\Util\Filesystem; class FileDownloaderTest extends \PHPUnit_Framework_TestCase { - protected function getDownloader($io = null, $rfs = null) + protected function getDownloader($io = null, $config = null, $rfs = null) { $io = $io ?: $this->getMock('Composer\IO\IOInterface'); + $config = $config ?: $this->getMock('Composer\Config'); $rfs = $rfs ?: $this->getMockBuilder('Composer\Util\RemoteFilesystem')->disableOriginalConstructor()->getMock(); - return new FileDownloader($io, $rfs); + return new FileDownloader($io, $config, $rfs); } /** diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index b852329b0..899d10b49 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -32,7 +32,8 @@ class ZipDownloaderTest extends \PHPUnit_Framework_TestCase ; $io = $this->getMock('Composer\IO\IOInterface'); - $downloader = new ZipDownloader($io); + $config = $this->getMock('Composer\Config'); + $downloader = new ZipDownloader($io, $config); try { $downloader->download($packageMock, sys_get_temp_dir().'/composer-zip-test');