diff --git a/doc/04-schema.md b/doc/04-schema.md index ec885bf85..c4de65150 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -655,6 +655,8 @@ The following options are supported: * **prepend-autoloader:** Defaults to `true`. If false, the composer autoloader will not be prepended to existing autoloaders. This is sometimesrequired to fix interoperability issues with other autoloaders. +* **github-domains:** Defaults to `["github.com"]`. A list of domains to use in + github mode. This is used for GitHub Enterprise setups. * **notify-on-install:** Defaults to `true`. Composer allows repositories to define a notification URL, so that they get notified whenever a package from that repository is installed. This option allows you to disable that behaviour. diff --git a/res/composer-schema.json b/res/composer-schema.json index 1f68c8b43..7b52d7733 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -179,6 +179,13 @@ "prepend-autoloader": { "type": "boolean", "description": "If false, the composer autoloader will not be prepended to existing autoloaders, defaults to true." + }, + "github-domains": { + "type": "array", + "description": "A list of domains to use in github mode. This is used for GitHub Enterprise setups, defaults to [\"github.com\"].", + "items": { + "type": "string" + } } } }, diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index 7f92b2503..edc370a66 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -308,6 +308,18 @@ EOT return $vals; } ), + 'github-domains' => array( + function ($vals) { + if (!is_array($vals)) { + return 'array expected'; + } + + return true; + }, + function ($vals) { + return $vals; + } + ), ); foreach ($uniqueConfigValues as $name => $callbacks) { diff --git a/src/Composer/Config.php b/src/Composer/Config.php index 6151b4f81..c893091c6 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -36,6 +36,7 @@ class Config 'cache-files-maxsize' => '300MiB', 'discard-changes' => false, 'prepend-autoloader' => true, + 'github-domains' => array('github.com'), ); public static $defaultRepositories = array( diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index f320cf931..56b55629e 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -293,7 +293,7 @@ class GitDownloader extends VcsDownloader } // public github, autoswitch protocols - if (preg_match('{^(?:https?|git)(://github.com/.*)}', $url, $match)) { + if (preg_match('{^(?:https?|git)(://'.$this->getGitHubDomainsRegex().'/.*)}', $url, $match)) { $protocols = $this->config->get('github-protocols'); if (!is_array($protocols)) { throw new \RuntimeException('Config value "github-protocols" must be an array, got '.gettype($protocols)); @@ -317,7 +317,7 @@ class GitDownloader extends VcsDownloader $command = call_user_func($commandCallable, $url); if (0 !== $this->process->execute($command, $ignoredOutput, $cwd)) { // private github repository without git access, try https with auth - if (preg_match('{^git@(github.com):(.+?)\.git$}i', $url, $match)) { + if (preg_match('{^git@'.$this->getGitHubDomainsRegex().':(.+?)\.git$}i', $url, $match)) { if (!$this->io->hasAuthentication($match[1])) { $gitHubUtil = new GitHub($this->io, $this->config, $this->process); $message = 'Cloning failed using an ssh key for authentication, enter your GitHub credentials to access private repos'; @@ -368,6 +368,11 @@ class GitDownloader extends VcsDownloader } } + protected function getGitHubDomainsRegex() + { + return '('.implode('|', array_map('preg_quote', $this->config->get('github-domains'))).')'; + } + protected function throwException($message, $url) { if (0 !== $this->process->execute('git --version', $ignoredOutput)) { @@ -385,11 +390,11 @@ class GitDownloader extends VcsDownloader protected function setPushUrl(PackageInterface $package, $path) { // set push url for github projects - if (preg_match('{^(?:https?|git)://github.com/([^/]+)/([^/]+?)(?:\.git)?$}', $package->getSourceUrl(), $match)) { + if (preg_match('{^(?:https?|git)://'.$this->getGitHubDomainsRegex().'/([^/]+)/([^/]+?)(?:\.git)?$}', $package->getSourceUrl(), $match)) { $protocols = $this->config->get('github-protocols'); - $pushUrl = 'git@github.com:'.$match[1].'/'.$match[2].'.git'; + $pushUrl = 'git@'.$match[1].':'.$match[2].'/'.$match[3].'.git'; if ($protocols[0] !== 'git') { - $pushUrl = 'https://github.com/'.$match[1].'/'.$match[2].'.git'; + $pushUrl = 'https://' . $match[1] . '/'.$match[2].'/'.$match[3].'.git'; } $cmd = sprintf('git remote set-url --push origin %s', escapeshellarg($pushUrl)); $this->process->execute($cmd, $ignoredOutput, $path); diff --git a/src/Composer/Repository/Vcs/GitBitbucketDriver.php b/src/Composer/Repository/Vcs/GitBitbucketDriver.php index 950115a79..12eba6d3a 100644 --- a/src/Composer/Repository/Vcs/GitBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/GitBitbucketDriver.php @@ -12,6 +12,7 @@ namespace Composer\Repository\Vcs; +use Composer\Config; use Composer\Json\JsonFile; use Composer\IO\IOInterface; @@ -140,7 +141,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface /** * {@inheritDoc} */ - public static function supports(IOInterface $io, $url, $deep = false) + public static function supports(IOInterface $io, Config $config, $url, $deep = false) { if (!preg_match('#^https://bitbucket\.org/([^/]+)/(.+?)\.git$#', $url)) { return false; diff --git a/src/Composer/Repository/Vcs/GitDriver.php b/src/Composer/Repository/Vcs/GitDriver.php index e2a3a9eb0..ecfad77db 100644 --- a/src/Composer/Repository/Vcs/GitDriver.php +++ b/src/Composer/Repository/Vcs/GitDriver.php @@ -18,6 +18,7 @@ use Composer\Util\Filesystem; use Composer\Util\Git as GitUtil; use Composer\IO\IOInterface; use Composer\Cache; +use Composer\Config; /** * @author Jordi Boggiano @@ -211,7 +212,7 @@ class GitDriver extends VcsDriver /** * {@inheritDoc} */ - public static function supports(IOInterface $io, $url, $deep = false) + public static function supports(IOInterface $io, Config $config, $url, $deep = false) { if (preg_match('#(^git://|\.git$|git(?:olite)?@|//git\.|//github.com/)#i', $url)) { return true; diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index 6345fa473..428c2fdf4 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -12,6 +12,7 @@ namespace Composer\Repository\Vcs; +use Composer\Config; use Composer\Downloader\TransportException; use Composer\Json\JsonFile; use Composer\Cache; @@ -45,10 +46,10 @@ class GitHubDriver extends VcsDriver */ public function initialize() { - preg_match('#^(?:(?:https?|git)://github\.com/|git@github\.com:)([^/]+)/(.+?)(?:\.git)?$#', $this->url, $match); - $this->owner = $match[1]; - $this->repository = $match[2]; - $this->originUrl = 'github.com'; + preg_match('#^(?:(?:https?|git)://([^/]+)/|git@([^:]+):)([^/]+)/(.+?)(?:\.git)?$#', $this->url, $match); + $this->owner = $match[3]; + $this->repository = $match[4]; + $this->originUrl = isset($match[1]) ? $match[1] : $match[2]; $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.$this->originUrl.'/'.$this->owner.'/'.$this->repository); $this->fetchRootIdentifier(); @@ -75,7 +76,21 @@ class GitHubDriver extends VcsDriver return $this->gitDriver->getUrl(); } - return 'https://github.com/'.$this->owner.'/'.$this->repository.'.git'; + return 'https://' . $this->originUrl . '/'.$this->owner.'/'.$this->repository.'.git'; + } + + /** + * {@inheritDoc} + */ + protected function getApiUrl() + { + if ('github.com' === $this->originUrl) { + $apiUrl = 'api.github.com'; + } else { + $apiUrl = $this->originUrl . '/api/v3'; + } + + return 'https://' . $apiUrl; } /** @@ -105,7 +120,8 @@ class GitHubDriver extends VcsDriver if ($this->gitDriver) { return $this->gitDriver->getDist($identifier); } - $url = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/zipball/'.$identifier; + + $url = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/zipball/'.$identifier; return array('type' => 'zip', 'url' => $url, 'reference' => $identifier, 'shasum' => ''); } @@ -127,7 +143,7 @@ class GitHubDriver extends VcsDriver $notFoundRetries = 2; while ($notFoundRetries) { try { - $resource = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/contents/composer.json?ref='.urlencode($identifier); + $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/contents/composer.json?ref='.urlencode($identifier); $composer = JsonFile::parseJson($this->getContents($resource)); if (empty($composer['content']) || $composer['encoding'] !== 'base64' || !($composer = base64_decode($composer['content']))) { throw new \RuntimeException('Could not retrieve composer.json from '.$resource); @@ -149,16 +165,16 @@ class GitHubDriver extends VcsDriver $composer = JsonFile::parseJson($composer, $resource); if (!isset($composer['time'])) { - $resource = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/commits/'.urlencode($identifier); + $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/commits/'.urlencode($identifier); $commit = JsonFile::parseJson($this->getContents($resource), $resource); $composer['time'] = $commit['commit']['committer']['date']; } if (!isset($composer['support']['source'])) { $label = array_search($identifier, $this->getTags()) ?: array_search($identifier, $this->getBranches()) ?: $identifier; - $composer['support']['source'] = sprintf('https://github.com/%s/%s/tree/%s', $this->owner, $this->repository, $label); + $composer['support']['source'] = sprintf('https://%s/%s/%s/tree/%s', $this->originUrl, $this->owner, $this->repository, $label); } if (!isset($composer['support']['issues']) && $this->hasIssues) { - $composer['support']['issues'] = sprintf('https://github.com/%s/%s/issues', $this->owner, $this->repository); + $composer['support']['issues'] = sprintf('https://%s/%s/%s/issues', $this->originUrl, $this->owner, $this->repository); } } @@ -181,7 +197,7 @@ class GitHubDriver extends VcsDriver return $this->gitDriver->getTags(); } if (null === $this->tags) { - $resource = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/tags'; + $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/tags'; $tagsData = JsonFile::parseJson($this->getContents($resource), $resource); $this->tags = array(); foreach ($tagsData as $tag) { @@ -201,7 +217,7 @@ class GitHubDriver extends VcsDriver return $this->gitDriver->getBranches(); } if (null === $this->branches) { - $resource = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/git/refs/heads'; + $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/git/refs/heads'; $branchData = JsonFile::parseJson($this->getContents($resource), $resource); $this->branches = array(); foreach ($branchData as $branch) { @@ -216,9 +232,14 @@ class GitHubDriver extends VcsDriver /** * {@inheritDoc} */ - public static function supports(IOInterface $io, $url, $deep = false) + public static function supports(IOInterface $io, Config $config, $url, $deep = false) { - if (!preg_match('#^((?:https?|git)://github\.com/|git@github\.com:)([^/]+)/(.+?)(?:\.git)?$#', $url)) { + if (!preg_match('#^((?:https?|git)://([^/]+)/|git@([^:]+):)([^/]+)/(.+?)(?:\.git)?$#', $url, $matches)) { + return false; + } + + $originUrl = isset($matches[2]) ? $matches[2] : $matches[3]; + if (!in_array($originUrl, $config->get('github-domains'))) { return false; } @@ -240,7 +261,7 @@ class GitHubDriver extends VcsDriver */ protected function generateSshUrl() { - return 'git@github.com:'.$this->owner.'/'.$this->repository.'.git'; + return 'git@' . $this->originUrl . ':'.$this->owner.'/'.$this->repository.'.git'; } /** @@ -357,7 +378,7 @@ class GitHubDriver extends VcsDriver */ protected function fetchRootIdentifier() { - $repoDataUrl = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository; + $repoDataUrl = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository; $repoData = JsonFile::parseJson($this->getContents($repoDataUrl, true), $repoDataUrl); if (null === $repoData && null !== $this->gitDriver) { diff --git a/src/Composer/Repository/Vcs/HgBitbucketDriver.php b/src/Composer/Repository/Vcs/HgBitbucketDriver.php index 80683a465..c6eac73b9 100644 --- a/src/Composer/Repository/Vcs/HgBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/HgBitbucketDriver.php @@ -12,6 +12,7 @@ namespace Composer\Repository\Vcs; +use Composer\Config; use Composer\Json\JsonFile; use Composer\IO\IOInterface; @@ -150,7 +151,7 @@ class HgBitbucketDriver extends VcsDriver /** * {@inheritDoc} */ - public static function supports(IOInterface $io, $url, $deep = false) + public static function supports(IOInterface $io, Config $config, $url, $deep = false) { if (!preg_match('#^https://bitbucket\.org/([^/]+)/([^/]+)/?$#', $url)) { return false; diff --git a/src/Composer/Repository/Vcs/HgDriver.php b/src/Composer/Repository/Vcs/HgDriver.php index c3c06e38a..5de081cca 100644 --- a/src/Composer/Repository/Vcs/HgDriver.php +++ b/src/Composer/Repository/Vcs/HgDriver.php @@ -12,6 +12,7 @@ namespace Composer\Repository\Vcs; +use Composer\Config; use Composer\Json\JsonFile; use Composer\Util\ProcessExecutor; use Composer\Util\Filesystem; @@ -189,7 +190,7 @@ class HgDriver extends VcsDriver /** * {@inheritDoc} */ - public static function supports(IOInterface $io, $url, $deep = false) + public static function supports(IOInterface $io, Config $config, $url, $deep = false) { if (preg_match('#(^(?:https?|ssh)://(?:[^@]@)?bitbucket.org|https://(?:.*?)\.kilnhg.com)#i', $url)) { return true; diff --git a/src/Composer/Repository/Vcs/PerforceDriver.php b/src/Composer/Repository/Vcs/PerforceDriver.php index 54e1d3e7d..484f1e96f 100644 --- a/src/Composer/Repository/Vcs/PerforceDriver.php +++ b/src/Composer/Repository/Vcs/PerforceDriver.php @@ -12,6 +12,7 @@ namespace Composer\Repository\Vcs; +use Composer\Config; use Composer\IO\IOInterface; use Composer\Util\ProcessExecutor; use Composer\Util\Perforce; @@ -158,7 +159,7 @@ class PerforceDriver extends VcsDriver /** * {@inheritDoc} */ - public static function supports(IOInterface $io, $url, $deep = false) + public static function supports(IOInterface $io, Config $config, $url, $deep = false) { if ($deep || preg_match('#\b(perforce|p4)\b#i', $url)) { return Perforce::checkServerExists($url, new ProcessExecutor); diff --git a/src/Composer/Repository/Vcs/SvnDriver.php b/src/Composer/Repository/Vcs/SvnDriver.php index c5a67b455..d69230cce 100644 --- a/src/Composer/Repository/Vcs/SvnDriver.php +++ b/src/Composer/Repository/Vcs/SvnDriver.php @@ -13,6 +13,7 @@ namespace Composer\Repository\Vcs; use Composer\Cache; +use Composer\Config; use Composer\Json\JsonFile; use Composer\Util\ProcessExecutor; use Composer\Util\Filesystem; @@ -241,7 +242,7 @@ class SvnDriver extends VcsDriver /** * {@inheritDoc} */ - public static function supports(IOInterface $io, $url, $deep = false) + public static function supports(IOInterface $io, Config $config, $url, $deep = false) { $url = self::normalizeUrl($url); if (preg_match('#(^svn://|^svn\+ssh://|svn\.)#i', $url)) { diff --git a/src/Composer/Repository/Vcs/VcsDriverInterface.php b/src/Composer/Repository/Vcs/VcsDriverInterface.php index b29841b68..dd30baacd 100644 --- a/src/Composer/Repository/Vcs/VcsDriverInterface.php +++ b/src/Composer/Repository/Vcs/VcsDriverInterface.php @@ -12,6 +12,7 @@ namespace Composer\Repository\Vcs; +use Composer\Config; use Composer\IO\IOInterface; /** @@ -90,10 +91,11 @@ interface VcsDriverInterface /** * Checks if this driver can handle a given url * - * @param IOInterface $io IO instance - * @param string $url - * @param bool $deep unless true, only shallow checks (url matching typically) should be done + * @param IOInterface $io IO instance + * @param Config $config current $config + * @param string $url URL to validate/check + * @param bool $deep unless true, only shallow checks (url matching typically) should be done * @return bool */ - public static function supports(IOInterface $io, $url, $deep = false); + public static function supports(IOInterface $io, Config $config, $url, $deep = false); } diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index 701db33bb..c7a543748 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -80,7 +80,7 @@ class VcsRepository extends ArrayRepository } foreach ($this->drivers as $driver) { - if ($driver::supports($this->io, $this->url)) { + if ($driver::supports($this->io, $this->config, $this->url)) { $driver = new $driver($this->repoConfig, $this->io, $this->config); $driver->initialize(); @@ -89,7 +89,7 @@ class VcsRepository extends ArrayRepository } foreach ($this->drivers as $driver) { - if ($driver::supports($this->io, $this->url, true)) { + if ($driver::supports($this->io, $this->config, $this->url, true)) { $driver = new $driver($this->repoConfig, $this->io, $this->config); $driver->initialize(); diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index 93894cc76..49e56f8c9 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -51,7 +51,7 @@ class GitHub */ public function authorizeOAuth($originUrl) { - if ('github.com' !== $originUrl) { + if (!in_array($originUrl, $this->config->get('github-domains'))) { return false; } @@ -78,6 +78,8 @@ class GitHub { $attemptCounter = 0; + $apiUrl = ('github.com' === $originUrl) ? 'api.github.com' : $originUrl . '/api/v3'; + if ($message) { $this->io->write($message); } @@ -95,7 +97,7 @@ class GitHub $appName .= ' on ' . trim($output); } - $contents = JsonFile::parseJson($this->remoteFilesystem->getContents($originUrl, 'https://api.github.com/authorizations', false, array( + $contents = JsonFile::parseJson($this->remoteFilesystem->getContents($originUrl, 'https://'. $apiUrl . '/authorizations', false, array( 'http' => array( 'method' => 'POST', 'follow_location' => false, diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 1d093574a..7a0816eaf 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -24,10 +24,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase $executor = $executor ?: $this->getMock('Composer\Util\ProcessExecutor'); $filesystem = $filesystem ?: $this->getMock('Composer\Util\Filesystem'); if (!$config) { - $config = $this->getMock('Composer\Config'); - $config->expects($this->any()) - ->method('has') - ->will($this->returnValue(false)); + $config = new Config(); } return new GitDownloader($io, $config, $executor, $filesystem); diff --git a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php index d2c8167b9..36cd69ebc 100644 --- a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php @@ -104,7 +104,6 @@ class PerforceDriverTest extends \PHPUnit_Framework_TestCase public function testHasComposerFile() { - $this->setUp(); $repoConfig = array( 'url' => 'TEST_PERFORCE_URL', 'depot' => 'TEST_DEPOT_CONFIG', @@ -131,17 +130,17 @@ class PerforceDriverTest extends \PHPUnit_Framework_TestCase $result = $driver->hasComposerFile($identifier); $this->assertTrue($result); } - + /** * Test that supports() simply return false. - * + * * @covers \Composer\Repository\Vcs\PerforceDriver::supports - * + * * @return void */ public function testSupportsReturnsFalseNoDeepCheck() { $this->expectOutputString(''); - $this->assertFalse(PerforceDriver::supports($this->io, 'existing.url')); + $this->assertFalse(PerforceDriver::supports($this->io, $this->config, 'existing.url')); } } diff --git a/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php b/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php index d9bd53321..2f698565f 100644 --- a/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php @@ -80,10 +80,8 @@ class SvnDriverTest extends \PHPUnit_Framework_TestCase */ public function testSupport($url, $assertion) { - if ($assertion === true) { - $this->assertTrue(SvnDriver::supports($this->getMock('Composer\IO\IOInterface'), $url)); - } else { - $this->assertFalse(SvnDriver::supports($this->getMock('Composer\IO\IOInterface'), $url)); - } + $config = new Config(); + $result = SvnDriver::supports($this->getMock('Composer\IO\IOInterface'), $config, $url); + $this->assertEquals($assertion, $result); } }