From d5332a1b5c87618d83e4257797e104ce3b8abc22 Mon Sep 17 00:00:00 2001 From: Paul Wenke Date: Sun, 6 Mar 2016 22:05:00 -0500 Subject: [PATCH 1/6] Developed bitbucket-oauth functionality. --- src/Composer/Command/ConfigCommand.php | 11 ++ src/Composer/Config.php | 4 + src/Composer/Config/JsonConfigSource.php | 2 +- src/Composer/IO/BaseIO.php | 5 + src/Composer/Util/Bitbucket.php | 183 +++++++++++++++++++++ src/Composer/Util/Git.php | 33 +++- src/Composer/Util/RemoteFilesystem.php | 5 + src/Composer/Util/StreamContextFactory.php | 4 + 8 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 src/Composer/Util/Bitbucket.php diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index a3e590dc2..66fca1550 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -491,6 +491,17 @@ EOT return; } + // handle bitbucket-oauth + if (preg_match('/^(bitbucket-oauth)\.(.+)/', $settingKey, $matches)) { + if (2 !== count($values)) { + throw new \RuntimeException('Excepted two arguments (consumer-key, consumer-secret), got '.count($values)); + } + $this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]); + $this->authConfigSource->addConfigSetting($matches[1].'.'.$matches[2], array('consumer-key' => $values[0], 'consumer-secret' => $values[1])); + + return; + } + throw new \InvalidArgumentException('Setting '.$settingKey.' does not exist or is not supported by this command'); } diff --git a/src/Composer/Config.php b/src/Composer/Config.php index e326cda2e..982f70e62 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -27,6 +27,7 @@ class Config 'preferred-install' => 'auto', 'notify-on-install' => true, 'github-protocols' => array('https', 'ssh', 'git'), + 'bitbucket-protocols' => array('https'), 'vendor-dir' => 'vendor', 'bin-dir' => '{$vendor-dir}/bin', 'cache-dir' => '{$home}/cache', @@ -45,6 +46,8 @@ class Config 'classmap-authoritative' => false, 'prepend-autoloader' => true, 'github-domains' => array('github.com'), + 'bitbucket-domains' => array('bitbucket.org'), + 'bitbucket-expose-hostname' => true, 'disable-tls' => false, 'secure-http' => true, 'cafile' => null, @@ -59,6 +62,7 @@ class Config // github-oauth // gitlab-oauth // http-basic + // bitbucket-oauth ); public static $defaultRepositories = array( diff --git a/src/Composer/Config/JsonConfigSource.php b/src/Composer/Config/JsonConfigSource.php index 6a4682539..9da47d0c3 100644 --- a/src/Composer/Config/JsonConfigSource.php +++ b/src/Composer/Config/JsonConfigSource.php @@ -81,7 +81,7 @@ class JsonConfigSource implements ConfigSourceInterface { $authConfig = $this->authConfig; $this->manipulateJson('addConfigSetting', $name, $value, function (&$config, $key, $val) use ($authConfig) { - if (preg_match('{^(github-oauth|gitlab-oauth|http-basic|platform)\.}', $key)) { + if (preg_match('{^(github-oauth|gitlab-oauth|http-basic|platform|bitbucket)\.}', $key)) { list($key, $host) = explode('.', $key, 2); if ($authConfig) { $config[$key][$host] = $val; diff --git a/src/Composer/IO/BaseIO.php b/src/Composer/IO/BaseIO.php index 0a787029c..35cf10c42 100644 --- a/src/Composer/IO/BaseIO.php +++ b/src/Composer/IO/BaseIO.php @@ -88,6 +88,7 @@ abstract class BaseIO implements IOInterface $githubOauth = $config->get('github-oauth') ?: array(); $gitlabOauth = $config->get('gitlab-oauth') ?: array(); $httpBasic = $config->get('http-basic') ?: array(); + $bitbucketOauth = $config->get('bitbucket-oauth') ?: array(); // reload oauth token from config if available foreach ($githubOauth as $domain => $token) { @@ -106,6 +107,10 @@ abstract class BaseIO implements IOInterface $this->checkAndSetAuthentication($domain, $cred['username'], $cred['password']); } + foreach ($bitbucketOauth as $domain => $cred) { + $this->checkAndSetAuthentication($domain, $cred['consumer-key'], $cred['consumer-secret']); + } + // setup process timeout ProcessExecutor::setTimeout((int) $config->get('process-timeout')); } diff --git a/src/Composer/Util/Bitbucket.php b/src/Composer/Util/Bitbucket.php new file mode 100644 index 000000000..153a61285 --- /dev/null +++ b/src/Composer/Util/Bitbucket.php @@ -0,0 +1,183 @@ + + * 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\Factory; +use Composer\IO\IOInterface; +use Composer\Config; +use Composer\Downloader\TransportException; + +/** + * @author Paul Wenke + */ +class Bitbucket +{ + protected $io; + protected $config; + protected $process; + protected $remoteFilesystem; + protected $token = array(); + + /** + * 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 ?: Factory::createRemoteFilesystem($this->io, $config); + } + + /** + * @return array + */ + public function getToken() + { + return $this->token; + } + + /** + * Attempts to authorize a Bitbucket domain via OAuth + * + * @param string $originUrl The host this Bitbucket instance is located at + * @return bool true on success + */ + public function authorizeOAuth($originUrl) + { + if (!in_array($originUrl, $this->config->get('bitbucket-domains'))) { + return false; + } + + // if available use token from git config + if (0 === $this->process->execute('git config bitbucket.accesstoken', $output)) { + $this->io->setAuthentication($originUrl, trim($output), 'x-oauth-basic'); + + return true; + } + + return false; + } + + /** + * @param string $originUrl + * @return bool + */ + private function requestAccessToken($originUrl) + { + try { + $apiUrl = 'bitbucket.org/site/oauth2/access_token'; + + $json = $this->remoteFilesystem->getContents($originUrl, 'https://'.$apiUrl, false, array( + 'retry-auth-failure' => false, + )); + + $this->token = json_decode($json, true); + } catch (TransportException $e) { + if (in_array($e->getCode(), array(403, 401))) { + $this->io->writeError('Invalid consumer provided.'); + $this->io->writeError('You can also add it manually later by using "composer config bitbucket-oauth.bitbucket.org "'); + + return false; + } + + throw $e; + } + } + + /** + * Authorizes a Bitbucket domain interactively via OAuth + * + * @param string $originUrl The host this Bitbucket instance is located at + * @param string $message The reason this authorization is required + * @throws \RuntimeException + * @throws TransportException|\Exception + * @return bool true on success + */ + public function authorizeOAuthInteractively($originUrl, $message = null) + { + if ($message) { + $this->io->writeError($message); + } + + $note = 'Composer'; + if ($this->config->get('bitbucket-expose-hostname') === true && 0 === $this->process->execute('hostname', $output)) { + $note .= ' on ' . trim($output); + } + $note .= ' ' . date('Y-m-d Hi'); + + $url = 'https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html'; + $this->io->writeError(sprintf('Follow the instructions on %s', $url)); + $this->io->writeError(sprintf('to create a consumer. It will be stored in "%s" for future use by Composer.', $this->config->getAuthConfigSource()->getName())); + + $consumerKey = trim($this->io->askAndHideAnswer('Consumer Key (hidden): ')); + + if (!$consumerKey) { + $this->io->writeError('No consumer key given, aborting.'); + $this->io->writeError('You can also add it manually later by using "composer config bitbucket-oauth.bitbucket.org "'); + + return false; + } + + $consumerSecret = trim($this->io->askAndHideAnswer('Consumer Secret (hidden): ')); + + if (!$consumerSecret) { + $this->io->writeError('No consumer secret given, aborting.'); + $this->io->writeError('You can also add it manually later by using "composer config bitbucket-oauth.bitbucket.org "'); + + return false; + } + + $this->io->setAuthentication($originUrl, $consumerKey, $consumerSecret); + + $this->requestAccessToken($originUrl); + + // store value in user config + $this->config->getConfigSource()->removeConfigSetting('bitbucket-oauth.'.$originUrl); + + $consumer = array( + "consumer-key" => $consumerKey, + "consumer-secret" => $consumerSecret + ); + $this->config->getAuthConfigSource()->addConfigSetting('bitbucket-oauth.'.$originUrl, $consumer); + + $this->io->writeError('Consumer stored successfully.'); + + return true; + } + + /** + * Retrieves an access token from Bitbucket. + * + * @param string $originUrl + * @param string $consumerKey + * @param string $consumerSecret + * @return array + */ + public function requestToken($originUrl, $consumerKey, $consumerSecret) + { + if (!empty($this->token)) { + return $this->token; + } + + $this->io->setAuthentication($originUrl, $consumerKey, $consumerSecret); + $this->requestAccessToken($originUrl); + + return $this->token; + } +} diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index 13ee9aecf..d63bdd468 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -108,7 +108,33 @@ class Git if ($this->io->hasAuthentication($match[1])) { $auth = $this->io->getAuthentication($match[1]); - $authUrl = 'https://'.rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@'.$match[1].'/'.$match[2].'.git'; + $authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[1] . '/' . $match[2] . '.git'; + $command = call_user_func($commandCallable, $authUrl); + if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { + return; + } + } + } elseif (preg_match('{^(?:https?|git)://'.self::getBitbucketDomainsRegex($this->config).'/(.*)\.git}', $url, $match)) { //bitbucket oauth + $bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process); + + if (!$this->io->hasAuthentication($match[1])) { + $message = 'Cloning failed using an ssh key for authentication, enter your Bitbucket credentials to access private repos'; + + if (!$bitbucketUtil->authorizeOAuth($match[1]) && $this->io->isInteractive()) { + $bitbucketUtil->authorizeOAuthInteractively($match[1], $message); + $token = $bitbucketUtil->getToken(); + $this->io->setAuthentication($match[1], 'x-token-auth', $token['access_token']); + } + } else { //We're authenticating with a locally stored consumer. + $auth = $this->io->getAuthentication($match[1]); + $token = $bitbucketUtil->requestToken($match[1], $auth['username'], $auth['password']); + $this->io->setAuthentication($match[1], 'x-token-auth', $token['access_token']); + } + + if ($this->io->hasAuthentication($match[1])) { + $auth = $this->io->getAuthentication($match[1]); + $authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[1] . '/' . $match[2] . '.git'; + $command = call_user_func($commandCallable, $authUrl); if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { return; @@ -214,6 +240,11 @@ class Git return '('.implode('|', array_map('preg_quote', $config->get('github-domains'))).')'; } + public static function getBitbucketDomainsRegex(Config $config) + { + return '('.implode('|', array_map('preg_quote', $config->get('bitbucket-domains'))).')'; + } + public static function sanitizeUrl($message) { return preg_replace('{://([^@]+?):.+?@}', '://$1:***@', $message); diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 7d37b5df3..cfd25aaf5 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -672,6 +672,11 @@ class RemoteFilesystem $authStr = base64_encode($auth['username'] . ':' . $auth['password']); $headers[] = 'Authorization: Basic '.$authStr; } + + if ('bitbucket.org' === $originUrl) { + $options['http']['method'] = 'POST'; + $options['http']['content']['grant_type'] = 'client_credentials'; + } } if (isset($options['http']['header']) && !is_array($options['http']['header'])) { diff --git a/src/Composer/Util/StreamContextFactory.php b/src/Composer/Util/StreamContextFactory.php index 484145f45..ad1f3b956 100644 --- a/src/Composer/Util/StreamContextFactory.php +++ b/src/Composer/Util/StreamContextFactory.php @@ -129,6 +129,10 @@ final class StreamContextFactory $options['http']['header'] = self::fixHttpHeaderField($options['http']['header']); } + if (isset($options['http']['content'])) { + $options['http']['content'] = http_build_query($options['http']['content']); + } + if (defined('HHVM_VERSION')) { $phpVersion = 'HHVM ' . HHVM_VERSION; } else { From d2c5479b2d298f4aa60b5b7006247eed191ac946 Mon Sep 17 00:00:00 2001 From: Paul Wenke Date: Sun, 6 Mar 2016 22:20:22 -0500 Subject: [PATCH 2/6] Updated documentation and fixed formatting in Bitbucket Util. --- doc/03-cli.md | 2 +- doc/06-config.md | 6 ++++++ src/Composer/Util/Bitbucket.php | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/doc/03-cli.md b/doc/03-cli.md index 8f621d11a..c56fd35b7 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -778,7 +778,7 @@ file to be used during SSL/TLS peer verification. The `COMPOSER_AUTH` var allows you to set up authentication as an environment variable. The contents of the variable should be a JSON formatted object containing http-basic, -github-oauth, ... objects as needed, and following the +github-oauth, bitbucket-oauth, ... objects as needed, and following the [spec from the config](06-config.md#gitlab-oauth). ### COMPOSER_DISCARD_CHANGES diff --git a/doc/06-config.md b/doc/06-config.md index 0dfd50bd0..fe4be2b76 100644 --- a/doc/06-config.md +++ b/doc/06-config.md @@ -81,6 +81,12 @@ downloaded via Composer. If you really absolutely need HTTP access to something then you can disable it, but using [Let's Encrypt](https://letsencrypt.org/) to get a free SSL certificate is generally a better alternative. +## bitbucket-oauth + +A list of domain names and consumers. For example using `{"bitbucket.org": +{"consumer-key": "myKey", "consumer-secret": "mySecret"}}`. [Read](https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html) +how to set up a consumer on Bitbucket. + ## cafile Location of Certificate Authority file on local filesystem. In PHP 5.6+ you diff --git a/src/Composer/Util/Bitbucket.php b/src/Composer/Util/Bitbucket.php index 153a61285..e27bd7439 100644 --- a/src/Composer/Util/Bitbucket.php +++ b/src/Composer/Util/Bitbucket.php @@ -91,7 +91,7 @@ class Bitbucket } catch (TransportException $e) { if (in_array($e->getCode(), array(403, 401))) { $this->io->writeError('Invalid consumer provided.'); - $this->io->writeError('You can also add it manually later by using "composer config bitbucket-oauth.bitbucket.org "'); + $this->io->writeError('You can also add it manually later by using "composer config bitbucket-oauth.bitbucket.org "'); return false; } From 9ddf8c4d55851c3a71da3cacddce7d60945318f1 Mon Sep 17 00:00:00 2001 From: "Paul.Wenke" Date: Sun, 13 Mar 2016 16:57:23 -0400 Subject: [PATCH 3/6] Fixed OAuth credentials issue for subsequent clone requests. --- src/Composer/Util/Git.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index d63bdd468..b839bdc56 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -114,7 +114,7 @@ class Git return; } } - } elseif (preg_match('{^(?:https?|git)://'.self::getBitbucketDomainsRegex($this->config).'/(.*)\.git}', $url, $match)) { //bitbucket oauth + } elseif (preg_match('{^(?:https?|git)://'.self::getBitbucketDomainsRegex($this->config).'/(.*)}', $url, $match)) { //bitbucket oauth $bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process); if (!$this->io->hasAuthentication($match[1])) { @@ -127,8 +127,12 @@ class Git } } else { //We're authenticating with a locally stored consumer. $auth = $this->io->getAuthentication($match[1]); - $token = $bitbucketUtil->requestToken($match[1], $auth['username'], $auth['password']); - $this->io->setAuthentication($match[1], 'x-token-auth', $token['access_token']); + + //We already have an access_token from a previous request. + if($auth['username'] !== 'x-token-auth') { + $token = $bitbucketUtil->requestToken($match[1], $auth['username'], $auth['password']); + $this->io->setAuthentication($match[1], 'x-token-auth', $token['access_token']); + } } if ($this->io->hasAuthentication($match[1])) { From b4d9d0fd0d4aa0c67023e67c07b41492ff8feab8 Mon Sep 17 00:00:00 2001 From: "Paul.Wenke" Date: Sun, 13 Mar 2016 21:33:13 -0400 Subject: [PATCH 4/6] Added BitbucketTest class. Added is_array and is_object check to http content before calling http_build_query. --- src/Composer/Util/Bitbucket.php | 1 + src/Composer/Util/StreamContextFactory.php | 2 +- tests/Composer/Test/Util/BitbucketTest.php | 137 +++++++++++++++++++++ 3 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 tests/Composer/Test/Util/BitbucketTest.php diff --git a/src/Composer/Util/Bitbucket.php b/src/Composer/Util/Bitbucket.php index e27bd7439..24ba98f98 100644 --- a/src/Composer/Util/Bitbucket.php +++ b/src/Composer/Util/Bitbucket.php @@ -18,6 +18,7 @@ use Composer\Config; use Composer\Downloader\TransportException; /** + * I used GitHub.php as a template. * @author Paul Wenke */ class Bitbucket diff --git a/src/Composer/Util/StreamContextFactory.php b/src/Composer/Util/StreamContextFactory.php index ad1f3b956..426eb0273 100644 --- a/src/Composer/Util/StreamContextFactory.php +++ b/src/Composer/Util/StreamContextFactory.php @@ -129,7 +129,7 @@ final class StreamContextFactory $options['http']['header'] = self::fixHttpHeaderField($options['http']['header']); } - if (isset($options['http']['content'])) { + if (isset($options['http']['content']) && (is_array($options['http']['content']) || is_object($options['http']['content']))) { $options['http']['content'] = http_build_query($options['http']['content']); } diff --git a/tests/Composer/Test/Util/BitbucketTest.php b/tests/Composer/Test/Util/BitbucketTest.php new file mode 100644 index 000000000..6decb5f74 --- /dev/null +++ b/tests/Composer/Test/Util/BitbucketTest.php @@ -0,0 +1,137 @@ + + * 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\Bitbucket; + +/** + * I used GitHubTest.php as a template. + * @author Paul Wenke + */ +class BitbucketTest extends \PHPUnit_Framework_TestCase +{ + private $username = 'username'; + private $password = 'password'; + private $authcode = 'authcode'; + private $message = 'mymessage'; + private $origin = 'bitbucket.org'; + private $token = 'bitbuckettoken'; + + public function testUsernamePasswordAuthenticationFlow() + { + $io = $this->getIOMock(); + $io + ->expects($this->at(0)) + ->method('writeError') + ->with($this->message) + ; + + $io->expects($this->exactly(2)) + ->method('askAndHideAnswer') + ->withConsecutive( + array('Consumer Key (hidden): '), + array('Consumer Secret (hidden): ') + ) + ->willReturnOnConsecutiveCalls($this->username, $this->password); + + $rfs = $this->getRemoteFilesystemMock(); + $rfs + ->expects($this->once()) + ->method('getContents') + ->with( + $this->equalTo($this->origin), + $this->equalTo(sprintf('https://%s/site/oauth2/access_token', $this->origin)), + $this->isFalse(), + $this->anything() + ) + ->willReturn(sprintf('{}', $this->token)) + ; + + $config = $this->getConfigMock(); + $config + ->expects($this->exactly(2)) + ->method('getAuthConfigSource') + ->willReturn($this->getAuthJsonMock()) + ; + $config + ->expects($this->once()) + ->method('getConfigSource') + ->willReturn($this->getConfJsonMock()) + ; + + $bitbucket = new Bitbucket($io, $config, null, $rfs); + + $this->assertTrue($bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); + } + + private function getIOMock() + { + $io = $this + ->getMockBuilder('Composer\IO\ConsoleIO') + ->disableOriginalConstructor() + ->getMock() + ; + + return $io; + } + + private function getConfigMock() + { + $config = $this->getMock('Composer\Config'); + + return $config; + } + + private function getRemoteFilesystemMock() + { + $rfs = $this + ->getMockBuilder('Composer\Util\RemoteFilesystem') + ->disableOriginalConstructor() + ->getMock() + ; + + return $rfs; + } + + private function getAuthJsonMock() + { + $authjson = $this + ->getMockBuilder('Composer\Config\JsonConfigSource') + ->disableOriginalConstructor() + ->getMock() + ; + $authjson + ->expects($this->atLeastOnce()) + ->method('getName') + ->willReturn('auth.json') + ; + + return $authjson; + } + + private function getConfJsonMock() + { + $confjson = $this + ->getMockBuilder('Composer\Config\JsonConfigSource') + ->disableOriginalConstructor() + ->getMock() + ; + $confjson + ->expects($this->atLeastOnce()) + ->method('removeConfigSetting') + ->with('bitbucket-oauth.'.$this->origin) + ; + + return $confjson; + } +} From 9059d70ba07ecad40011a056c985cc96354537f3 Mon Sep 17 00:00:00 2001 From: Paul Wenke Date: Sun, 20 Mar 2016 15:56:58 -0400 Subject: [PATCH 5/6] Corrected username / access token parameters for $this->io->setAuthentication when read from git config. Grant type is now only set when requesting an access token. Removed bitbucket-domains and bitbucket-protocols from config. Fixed bitbucket typo in JsonConfigSource. Removed unecessary comments. Changed visibility of Composer/Util/Bitbucket properties to private. Added https to bitbucket url. Removed unused $note variable. --- src/Composer/Config.php | 2 -- src/Composer/Config/JsonConfigSource.php | 2 +- src/Composer/Util/Bitbucket.php | 31 +++++++++++----------- src/Composer/Util/Git.php | 9 ++----- src/Composer/Util/RemoteFilesystem.php | 5 ---- tests/Composer/Test/Util/BitbucketTest.php | 1 - 6 files changed, 18 insertions(+), 32 deletions(-) diff --git a/src/Composer/Config.php b/src/Composer/Config.php index 982f70e62..8bf2e7a5a 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -27,7 +27,6 @@ class Config 'preferred-install' => 'auto', 'notify-on-install' => true, 'github-protocols' => array('https', 'ssh', 'git'), - 'bitbucket-protocols' => array('https'), 'vendor-dir' => 'vendor', 'bin-dir' => '{$vendor-dir}/bin', 'cache-dir' => '{$home}/cache', @@ -46,7 +45,6 @@ class Config 'classmap-authoritative' => false, 'prepend-autoloader' => true, 'github-domains' => array('github.com'), - 'bitbucket-domains' => array('bitbucket.org'), 'bitbucket-expose-hostname' => true, 'disable-tls' => false, 'secure-http' => true, diff --git a/src/Composer/Config/JsonConfigSource.php b/src/Composer/Config/JsonConfigSource.php index 9da47d0c3..ec777100b 100644 --- a/src/Composer/Config/JsonConfigSource.php +++ b/src/Composer/Config/JsonConfigSource.php @@ -81,7 +81,7 @@ class JsonConfigSource implements ConfigSourceInterface { $authConfig = $this->authConfig; $this->manipulateJson('addConfigSetting', $name, $value, function (&$config, $key, $val) use ($authConfig) { - if (preg_match('{^(github-oauth|gitlab-oauth|http-basic|platform|bitbucket)\.}', $key)) { + if (preg_match('{^(github-oauth|gitlab-oauth|http-basic|platform|bitbucket-oauth)\.}', $key)) { list($key, $host) = explode('.', $key, 2); if ($authConfig) { $config[$key][$host] = $val; diff --git a/src/Composer/Util/Bitbucket.php b/src/Composer/Util/Bitbucket.php index 24ba98f98..8246b3cb1 100644 --- a/src/Composer/Util/Bitbucket.php +++ b/src/Composer/Util/Bitbucket.php @@ -18,16 +18,15 @@ use Composer\Config; use Composer\Downloader\TransportException; /** - * I used GitHub.php as a template. * @author Paul Wenke */ class Bitbucket { - protected $io; - protected $config; - protected $process; - protected $remoteFilesystem; - protected $token = array(); + private $io; + private $config; + private $process; + private $remoteFilesystem; + private $token = array(); /** * Constructor. @@ -61,13 +60,13 @@ class Bitbucket */ public function authorizeOAuth($originUrl) { - if (!in_array($originUrl, $this->config->get('bitbucket-domains'))) { + if ($originUrl !== 'bitbucket.org') { return false; } // if available use token from git config if (0 === $this->process->execute('git config bitbucket.accesstoken', $output)) { - $this->io->setAuthentication($originUrl, trim($output), 'x-oauth-basic'); + $this->io->setAuthentication($originUrl, 'x-token-auth', trim($output)); return true; } @@ -82,10 +81,16 @@ class Bitbucket private function requestAccessToken($originUrl) { try { - $apiUrl = 'bitbucket.org/site/oauth2/access_token'; + $apiUrl = 'https://bitbucket.org/site/oauth2/access_token'; - $json = $this->remoteFilesystem->getContents($originUrl, 'https://'.$apiUrl, false, array( + $json = $this->remoteFilesystem->getContents($originUrl, $apiUrl, false, array( 'retry-auth-failure' => false, + 'http' => array( + 'method' => 'POST', + 'content' => array( + 'grant_type' => 'client_credentials' + ) + ) )); $this->token = json_decode($json, true); @@ -116,12 +121,6 @@ class Bitbucket $this->io->writeError($message); } - $note = 'Composer'; - if ($this->config->get('bitbucket-expose-hostname') === true && 0 === $this->process->execute('hostname', $output)) { - $note .= ' on ' . trim($output); - } - $note .= ' ' . date('Y-m-d Hi'); - $url = 'https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html'; $this->io->writeError(sprintf('Follow the instructions on %s', $url)); $this->io->writeError(sprintf('to create a consumer. It will be stored in "%s" for future use by Composer.', $this->config->getAuthConfigSource()->getName())); diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index b839bdc56..818a01aa5 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -114,11 +114,11 @@ class Git return; } } - } elseif (preg_match('{^(?:https?|git)://'.self::getBitbucketDomainsRegex($this->config).'/(.*)}', $url, $match)) { //bitbucket oauth + } elseif (preg_match('{^https://(bitbucket.org)/(.*)}', $url, $match)) { //bitbucket oauth $bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process); if (!$this->io->hasAuthentication($match[1])) { - $message = 'Cloning failed using an ssh key for authentication, enter your Bitbucket credentials to access private repos'; + $message = 'Enter your Bitbucket credentials to access private repos'; if (!$bitbucketUtil->authorizeOAuth($match[1]) && $this->io->isInteractive()) { $bitbucketUtil->authorizeOAuthInteractively($match[1], $message); @@ -244,11 +244,6 @@ class Git return '('.implode('|', array_map('preg_quote', $config->get('github-domains'))).')'; } - public static function getBitbucketDomainsRegex(Config $config) - { - return '('.implode('|', array_map('preg_quote', $config->get('bitbucket-domains'))).')'; - } - public static function sanitizeUrl($message) { return preg_replace('{://([^@]+?):.+?@}', '://$1:***@', $message); diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index cfd25aaf5..7d37b5df3 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -672,11 +672,6 @@ class RemoteFilesystem $authStr = base64_encode($auth['username'] . ':' . $auth['password']); $headers[] = 'Authorization: Basic '.$authStr; } - - if ('bitbucket.org' === $originUrl) { - $options['http']['method'] = 'POST'; - $options['http']['content']['grant_type'] = 'client_credentials'; - } } if (isset($options['http']['header']) && !is_array($options['http']['header'])) { diff --git a/tests/Composer/Test/Util/BitbucketTest.php b/tests/Composer/Test/Util/BitbucketTest.php index 6decb5f74..5be6da7b0 100644 --- a/tests/Composer/Test/Util/BitbucketTest.php +++ b/tests/Composer/Test/Util/BitbucketTest.php @@ -15,7 +15,6 @@ namespace Composer\Test\Util; use Composer\Util\Bitbucket; /** - * I used GitHubTest.php as a template. * @author Paul Wenke */ class BitbucketTest extends \PHPUnit_Framework_TestCase From 59ae2716aa1e59552474f18cce565db9a13d19be Mon Sep 17 00:00:00 2001 From: "Paul.Wenke" Date: Mon, 28 Mar 2016 09:13:58 -0400 Subject: [PATCH 6/6] Fixed typo in exception message for Bitbucket config command. --- src/Composer/Command/ConfigCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index 66fca1550..b83f599de 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -494,7 +494,7 @@ EOT // handle bitbucket-oauth if (preg_match('/^(bitbucket-oauth)\.(.+)/', $settingKey, $matches)) { if (2 !== count($values)) { - throw new \RuntimeException('Excepted two arguments (consumer-key, consumer-secret), got '.count($values)); + throw new \RuntimeException('Expected two arguments (consumer-key, consumer-secret), got '.count($values)); } $this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]); $this->authConfigSource->addConfigSetting($matches[1].'.'.$matches[2], array('consumer-key' => $values[0], 'consumer-secret' => $values[1]));