From 0e500d4c9169e5856a42b831b43668f7b032b767 Mon Sep 17 00:00:00 2001 From: Stefan Grootscholten Date: Mon, 16 May 2016 21:42:11 +0200 Subject: [PATCH 01/12] Make API URL available for external use. --- src/Composer/Util/Bitbucket.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Composer/Util/Bitbucket.php b/src/Composer/Util/Bitbucket.php index 89fdd3902..279ba83fc 100644 --- a/src/Composer/Util/Bitbucket.php +++ b/src/Composer/Util/Bitbucket.php @@ -28,6 +28,8 @@ class Bitbucket private $remoteFilesystem; private $token = array(); + const OAUTH2_ACCESS_TOKEN_URL = 'https://bitbucket.org/site/oauth2/access_token'; + /** * Constructor. * @@ -81,9 +83,7 @@ class Bitbucket private function requestAccessToken($originUrl) { try { - $apiUrl = 'https://bitbucket.org/site/oauth2/access_token'; - - $json = $this->remoteFilesystem->getContents($originUrl, $apiUrl, false, array( + $json = $this->remoteFilesystem->getContents($originUrl, self::OAUTH2_ACCESS_TOKEN_URL, false, array( 'retry-auth-failure' => false, 'http' => array( 'method' => 'POST', From 6c8b0cc2c17282acb57f3b3a83750b74b2fa65be Mon Sep 17 00:00:00 2001 From: Stefan Grootscholten Date: Mon, 16 May 2016 21:57:50 +0200 Subject: [PATCH 02/12] Change authentication for bitbucket to oauth. --- src/Composer/Util/RemoteFilesystem.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index ffbe693fc..b7cf90609 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -569,6 +569,25 @@ class RemoteFilesystem ) { throw new TransportException('Could not authenticate against '.$this->originUrl, 401); } + } elseif ($this->config && $this->originUrl === 'bitbucket.org') { + if (! $this->io->hasAuthentication($this->originUrl)) { + $message = "\n".'Could not fetch ' . $this->fileUrl . ', please create a bitbucket OAuth token to access private repos'; + $bitBucketUtil = new Bitbucket($this->io, $this->config); + if (! $bitBucketUtil->authorizeOAuth($this->originUrl) + && (! $this->io->isInteractive() || !$bitBucketUtil->authorizeOAuthInteractively($this->originUrl, $message)) + ) { + throw new TransportException('Could not authenticate against ' . $this->originUrl, 401); + } + } else { + $auth = $this->io->getAuthentication($this->originUrl); + if ($auth['username'] !== 'x-token-auth') { + $bitbucketUtil = new Bitbucket($this->io, $this->config); + $token = $bitbucketUtil->requestToken($this->originUrl, $auth['username'], $auth['password']); + $this->io->setAuthentication($this->originUrl, 'x-token-auth', $token['access_token']); + } else { + throw new TransportException('Could not authenticate against ' . $this->originUrl, 401); + } + } } else { // 404s are only handled for github if ($httpStatus === 404) { @@ -671,6 +690,10 @@ class RemoteFilesystem if ($auth['password'] === 'oauth2') { $headers[] = 'Authorization: Bearer '.$auth['username']; } + } elseif ('bitbucket.org' === $originUrl && $this->fileUrl !== Bitbucket::OAUTH2_ACCESS_TOKEN_URL) { + if ('x-token-auth' === $auth['username']) { + $headers[] = 'Authorization: Bearer ' . $auth['password']; + } } else { $authStr = base64_encode($auth['username'] . ':' . $auth['password']); $headers[] = 'Authorization: Basic '.$authStr; From 307fd4023bab7fa7a80325727b7558157f7f909c Mon Sep 17 00:00:00 2001 From: Stefan Grootscholten Date: Mon, 16 May 2016 22:05:54 +0200 Subject: [PATCH 03/12] Fetch composer.json using the oauth api. --- src/Composer/Repository/Vcs/GitBitbucketDriver.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Composer/Repository/Vcs/GitBitbucketDriver.php b/src/Composer/Repository/Vcs/GitBitbucketDriver.php index 7a2781ecb..9c5eb281d 100644 --- a/src/Composer/Repository/Vcs/GitBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/GitBitbucketDriver.php @@ -14,8 +14,10 @@ namespace Composer\Repository\Vcs; use Composer\Cache; use Composer\Config; +use Composer\Downloader\TransportException; use Composer\Json\JsonFile; use Composer\IO\IOInterface; +use Composer\Util\Bitbucket; /** * @author Per Bernhardt @@ -49,7 +51,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface { if (null === $this->rootIdentifier) { $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository; - $repoData = JsonFile::parseJson($this->getContents($resource), $resource); + $repoData = JsonFile::parseJson($this->getContents($resource, true), $resource); $this->rootIdentifier = !empty($repoData['main_branch']) ? $repoData['main_branch'] : 'master'; } @@ -92,13 +94,13 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface } if (!isset($this->infoCache[$identifier])) { - $resource = $this->getScheme() . '://bitbucket.org/'.$this->owner.'/'.$this->repository.'/raw/'.$identifier.'/composer.json'; - $composer = $this->getContents($resource); - if (!$composer) { + $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/src/'.$identifier.'/composer.json'; + $file = JsonFile::parseJson($this->getContents($resource), $resource); + if (!is_array($file) || ! array_key_exists('data', $file)) { return; } - $composer = JsonFile::parseJson($composer, $resource); + $composer = JsonFile::parseJson($file['data'], $resource); if (empty($composer['time'])) { $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/changesets/'.$identifier; From 1084a3927e3f43880b6d00d874335576faa93c13 Mon Sep 17 00:00:00 2001 From: Stefan Grootscholten Date: Sat, 21 May 2016 11:56:14 +0200 Subject: [PATCH 04/12] Implement Bitbucket Util in GitBitbucketDriver. --- .../Repository/Vcs/GitBitbucketDriver.php | 102 +++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/src/Composer/Repository/Vcs/GitBitbucketDriver.php b/src/Composer/Repository/Vcs/GitBitbucketDriver.php index 9c5eb281d..d70279016 100644 --- a/src/Composer/Repository/Vcs/GitBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/GitBitbucketDriver.php @@ -31,6 +31,10 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface protected $branches; protected $rootIdentifier; protected $infoCache = array(); + /** + * @var GitDriver + */ + protected $gitDriver; /** * {@inheritDoc} @@ -49,6 +53,10 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface */ public function getRootIdentifier() { + if ($this->gitDriver) { + return $this->gitDriver->getRootIdentifier(); + } + if (null === $this->rootIdentifier) { $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository; $repoData = JsonFile::parseJson($this->getContents($resource, true), $resource); @@ -63,7 +71,11 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface */ public function getUrl() { - return $this->url; + if ($this->gitDriver) { + return $this->gitDriver->getUrl(); + } + + return 'https://' . $this->originUrl . '/'.$this->owner.'/'.$this->repository.'.git'; } /** @@ -71,6 +83,10 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface */ public function getSource($identifier) { + if ($this->gitDriver) { + return $this->gitDriver->getSource($identifier); + } + return array('type' => 'git', 'url' => $this->getUrl(), 'reference' => $identifier); } @@ -89,6 +105,10 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface */ public function getComposerInformation($identifier) { + if ($this->gitDriver) { + return $this->gitDriver->getComposerInformation($identifier); + } + if (preg_match('{[a-f0-9]{40}}i', $identifier) && $res = $this->cache->read($identifier)) { $this->infoCache[$identifier] = JsonFile::parseJson($res); } @@ -123,6 +143,10 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface */ public function getTags() { + if ($this->gitDriver) { + return $this->gitDriver->getTags(); + } + if (null === $this->tags) { $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/tags'; $tagsData = JsonFile::parseJson($this->getContents($resource), $resource); @@ -140,6 +164,10 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface */ public function getBranches() { + if ($this->gitDriver) { + return $this->gitDriver->getBranches(); + } + if (null === $this->branches) { $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/branches'; $branchData = JsonFile::parseJson($this->getContents($resource), $resource); @@ -169,4 +197,76 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface return true; } + + protected function attemptCloneFallback() + { + try { + $this->setupGitDriver($this->generateSshUrl()); + + return; + } catch (\RuntimeException $e) { + $this->gitDriver = null; + + $this->io->writeError('Failed to clone the '.$this->generateSshUrl().' repository, try running in interactive mode so that you can enter your Bitbucket OAuth consumer credentials'); + throw $e; + } + } + + /** + * Generate an SSH URL + * + * @return string + */ + protected function generateSshUrl() + { + return 'git@' . $this->originUrl . ':' . $this->owner.'/'.$this->repository.'.git'; + } + + /** + * Get the remote content. + * + * @param string $url The URL of content + * @param bool $fetchingRepoData + * + * @return mixed The result + */ + protected function getContents($url, $fetchingRepoData = false) + { + try { + return parent::getContents($url); + } catch (TransportException $e) { + $bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process, $this->remoteFilesystem); + + switch ($e->getCode()) { + case 403: + if (!$this->io->hasAuthentication($this->originUrl) && $bitbucketUtil->authorizeOAuth($this->originUrl)) { + return parent::getContents($url); + } + + if (!$this->io->isInteractive() && $fetchingRepoData) { + return $this->attemptCloneFallback(); + } + + throw $e; + + default: + throw $e; + } + } + } + + /** + * @param string $url + */ + protected function setupGitDriver($url) + { + $this->gitDriver = new GitDriver( + array('url' => $url), + $this->io, + $this->config, + $this->process, + $this->remoteFilesystem + ); + $this->gitDriver->initialize(); + } } From d9fd9fca6b6e0d0fba3f88a733491bee11abf0df Mon Sep 17 00:00:00 2001 From: Stefan Grootscholten Date: Thu, 26 May 2016 12:32:09 +0200 Subject: [PATCH 05/12] Implement changes after review of stof. Rename getContents to getContentsWithOAuthCredentials. Make gitDriver a private property. --- .../Repository/Vcs/GitBitbucketDriver.php | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Composer/Repository/Vcs/GitBitbucketDriver.php b/src/Composer/Repository/Vcs/GitBitbucketDriver.php index d70279016..4ac356630 100644 --- a/src/Composer/Repository/Vcs/GitBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/GitBitbucketDriver.php @@ -24,6 +24,9 @@ use Composer\Util\Bitbucket; */ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface { + /** + * @var Cache + */ protected $cache; protected $owner; protected $repository; @@ -34,7 +37,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface /** * @var GitDriver */ - protected $gitDriver; + private $gitDriver; /** * {@inheritDoc} @@ -59,7 +62,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface if (null === $this->rootIdentifier) { $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository; - $repoData = JsonFile::parseJson($this->getContents($resource, true), $resource); + $repoData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource, true), $resource); $this->rootIdentifier = !empty($repoData['main_branch']) ? $repoData['main_branch'] : 'master'; } @@ -115,16 +118,16 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface if (!isset($this->infoCache[$identifier])) { $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/src/'.$identifier.'/composer.json'; - $file = JsonFile::parseJson($this->getContents($resource), $resource); + $file = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); if (!is_array($file) || ! array_key_exists('data', $file)) { - return; + return array(); } $composer = JsonFile::parseJson($file['data'], $resource); if (empty($composer['time'])) { $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/changesets/'.$identifier; - $changeset = JsonFile::parseJson($this->getContents($resource), $resource); + $changeset = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); $composer['time'] = $changeset['timestamp']; } @@ -149,7 +152,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface if (null === $this->tags) { $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/tags'; - $tagsData = JsonFile::parseJson($this->getContents($resource), $resource); + $tagsData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); $this->tags = array(); foreach ($tagsData as $tag => $data) { $this->tags[$tag] = $data['raw_node']; @@ -170,7 +173,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface if (null === $this->branches) { $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/branches'; - $branchData = JsonFile::parseJson($this->getContents($resource), $resource); + $branchData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); $this->branches = array(); foreach ($branchData as $branch => $data) { $this->branches[$branch] = $data['raw_node']; @@ -217,7 +220,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface * * @return string */ - protected function generateSshUrl() + private function generateSshUrl() { return 'git@' . $this->originUrl . ':' . $this->owner.'/'.$this->repository.'.git'; } @@ -230,7 +233,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface * * @return mixed The result */ - protected function getContents($url, $fetchingRepoData = false) + protected function getContentsWithOAuthCredentials($url, $fetchingRepoData = false) { try { return parent::getContents($url); @@ -258,7 +261,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface /** * @param string $url */ - protected function setupGitDriver($url) + private function setupGitDriver($url) { $this->gitDriver = new GitDriver( array('url' => $url), From 7716ef059b67fb4cee49bccfcc574dfc840e30e1 Mon Sep 17 00:00:00 2001 From: Stefan Grootscholten Date: Wed, 8 Jun 2016 20:56:11 +0200 Subject: [PATCH 06/12] Add the access token as query string parameter. --- src/Composer/Util/RemoteFilesystem.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index b7cf90609..3d0f0ae91 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -245,6 +245,11 @@ class RemoteFilesystem unset($options['gitlab-token']); } + if (isset($options['bitbucket-token'])) { + $fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['bitbucket-token']; + unset($options['bitbucket-token']); + } + if (isset($options['http'])) { $options['http']['ignore_errors'] = true; } @@ -692,7 +697,7 @@ class RemoteFilesystem } } elseif ('bitbucket.org' === $originUrl && $this->fileUrl !== Bitbucket::OAUTH2_ACCESS_TOKEN_URL) { if ('x-token-auth' === $auth['username']) { - $headers[] = 'Authorization: Bearer ' . $auth['password']; + $options['bitbucket-token'] = $auth['password']; } } else { $authStr = base64_encode($auth['username'] . ':' . $auth['password']); From ea11a95359ce942c8b867687699567c0979b0c17 Mon Sep 17 00:00:00 2001 From: Stefan Grootscholten Date: Wed, 8 Jun 2016 21:54:19 +0200 Subject: [PATCH 07/12] Provide fallback when using basic authentication. When composer is configured to use bitbucket basic authentication (a bitbucket username and password), you get an error when trying to install dependencies. This commit prevent the install process to be aborted due to uncaught exceptions. --- src/Composer/Util/Bitbucket.php | 11 +++++++++-- src/Composer/Util/Git.php | 4 +++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Composer/Util/Bitbucket.php b/src/Composer/Util/Bitbucket.php index 279ba83fc..e6e64df7a 100644 --- a/src/Composer/Util/Bitbucket.php +++ b/src/Composer/Util/Bitbucket.php @@ -93,8 +93,15 @@ class Bitbucket $this->token = json_decode($json, true); } catch (TransportException $e) { - if (in_array($e->getCode(), array(403, 401))) { - $this->io->writeError('Invalid consumer provided.'); + if ($e->getCode() === 400) { + $this->io->writeError('Invalid OAuth consumer provided.'); + $this->io->writeError('This can have two reasons:'); + $this->io->writeError('1. You are authenticating with a bitbucket username/password combination'); + $this->io->writeError('2. You are using an OAuth consumer, but didn\'t configure a (dummy) callback url'); + + return false; + } elseif (in_array($e->getCode(), array(403, 401))) { + $this->io->writeError('Invalid OAuth consumer provided.'); $this->io->writeError('You can also add it manually later by using "composer config bitbucket-oauth.bitbucket.org "'); return false; diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index 8d0c423bd..43422eea8 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -129,7 +129,9 @@ class Git //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 (! empty($token)) { + $this->io->setAuthentication($match[1], 'x-token-auth', $token['access_token']); + } } } From 42f4d344a491cfb48e8c8c7fef4b794bdf9641fc Mon Sep 17 00:00:00 2001 From: Stefan Grootscholten Date: Wed, 8 Jun 2016 21:59:00 +0200 Subject: [PATCH 08/12] Fix the replacement of the credentials. ://username:password@domain was replaced by ://://:***@domain instead of ://username:***@domain or ://***:***@domain --- src/Composer/Util/ProcessExecutor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index d2ced064f..709dcedd5 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -45,11 +45,11 @@ class ProcessExecutor { if ($this->io && $this->io->isDebug()) { $safeCommand = preg_replace_callback('{(://)(?P[^:/\s]+):(?P[^@\s/]+)}i', function ($m) { - if (preg_match('{^[a-f0-9]{12,}$}', $m[1])) { + if (preg_match('{^[a-f0-9]{12,}$}', $m[2])) { return '://***:***'; } - return '://'.$m[1].':***'; + return '://'.$m[2].':***'; }, $command); $this->io->writeError('Executing command ('.($cwd ?: 'CWD').'): '.$safeCommand); } From 1241e3e83c0b0e8d913c1e8e1236ada0d5922184 Mon Sep 17 00:00:00 2001 From: Stefan Grootscholten Date: Wed, 8 Jun 2016 22:13:49 +0200 Subject: [PATCH 09/12] Simplify the if statement. --- src/Composer/Util/RemoteFilesystem.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 3d0f0ae91..42026e086 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -695,10 +695,10 @@ class RemoteFilesystem if ($auth['password'] === 'oauth2') { $headers[] = 'Authorization: Bearer '.$auth['username']; } - } elseif ('bitbucket.org' === $originUrl && $this->fileUrl !== Bitbucket::OAUTH2_ACCESS_TOKEN_URL) { - if ('x-token-auth' === $auth['username']) { - $options['bitbucket-token'] = $auth['password']; - } + } elseif ('bitbucket.org' === $originUrl + && $this->fileUrl !== Bitbucket::OAUTH2_ACCESS_TOKEN_URL && 'x-token-auth' === $auth['username'] + ) { + $options['bitbucket-token'] = $auth['password']; } else { $authStr = base64_encode($auth['username'] . ':' . $auth['password']); $headers[] = 'Authorization: Basic '.$authStr; From 2d52531365d69229018576d2c3f4e122160731f4 Mon Sep 17 00:00:00 2001 From: Stefan Grootscholten Date: Sat, 11 Jun 2016 15:05:36 +0200 Subject: [PATCH 10/12] Add unit tests for the Bitbucket class. --- tests/Composer/Test/Util/BitbucketTest.php | 160 +++++++++++++++------ 1 file changed, 118 insertions(+), 42 deletions(-) diff --git a/tests/Composer/Test/Util/BitbucketTest.php b/tests/Composer/Test/Util/BitbucketTest.php index 5be6da7b0..9f6cd2a5f 100644 --- a/tests/Composer/Test/Util/BitbucketTest.php +++ b/tests/Composer/Test/Util/BitbucketTest.php @@ -21,30 +21,138 @@ class BitbucketTest extends \PHPUnit_Framework_TestCase { private $username = 'username'; private $password = 'password'; - private $authcode = 'authcode'; + private $consumer_key = 'consumer_key'; + private $consumer_secret = 'consumer_secret'; private $message = 'mymessage'; private $origin = 'bitbucket.org'; private $token = 'bitbuckettoken'; + /** @type \Composer\IO\ConsoleIO|\PHPUnit_Framework_MockObject_MockObject */ + private $io; + /** @type \Composer\Util\RemoteFilesystem|\PHPUnit_Framework_MockObject_MockObject */ + private $rfs; + /** @type \Composer\Config|\PHPUnit_Framework_MockObject_MockObject */ + private $config; + /** @type Bitbucket */ + private $bitbucket; + + protected function setUp() + { + $this->io = $this + ->getMockBuilder('Composer\IO\ConsoleIO') + ->disableOriginalConstructor() + ->getMock() + ; + + $this->rfs = $this + ->getMockBuilder('Composer\Util\RemoteFilesystem') + ->disableOriginalConstructor() + ->getMock() + ; + + $this->config = $this->getMock('Composer\Config'); + + $this->bitbucket = new Bitbucket($this->io, $this->config, null, $this->rfs); + } + + public function testRequestAccessTokenWithValidOAuthConsumer() + { + $this->io->expects($this->once()) + ->method('setAuthentication') + ->with($this->origin, $this->consumer_key, $this->consumer_secret); + + $this->rfs->expects($this->once()) + ->method('getContents') + ->with( + $this->origin, + Bitbucket::OAUTH2_ACCESS_TOKEN_URL, + false, + array( + 'retry-auth-failure' => false, + 'http' => array( + 'method' => 'POST', + 'content' => 'grant_type=client_credentials', + ) + ) + ) + ->willReturn( + sprintf( + '{"access_token": "%s", "scopes": "repository", "expires_in": 3600, "refresh_token": "refreshtoken", "token_type": "bearer"}', + $this->token + ) + ); + + $this->assertEquals( + array( + 'access_token' => $this->token, + 'scopes' => 'repository', + 'expires_in' => 3600, + 'refresh_token' => 'refreshtoken', + 'token_type' => 'bearer' + ), + $this->bitbucket->requestToken($this->origin, $this->consumer_key, $this->consumer_secret) + ); + } + + public function testRequestAccessTokenWithUsernameAndPassword() + { + $this->io->expects($this->once()) + ->method('setAuthentication') + ->with($this->origin, $this->username, $this->password); + + $this->io->expects($this->any()) + ->method('writeError') + ->withConsecutive( + array('Invalid OAuth consumer provided.'), + array('This can have two reasons:'), + array('1. You are authenticating with a bitbucket username/password combination'), + array('2. You are using an OAuth consumer, but didn\'t configure a (dummy) callback url') + ); + + $this->rfs->expects($this->once()) + ->method('getContents') + ->with( + $this->origin, + Bitbucket::OAUTH2_ACCESS_TOKEN_URL, + false, + array( + 'retry-auth-failure' => false, + 'http' => array( + 'method' => 'POST', + 'content' => 'grant_type=client_credentials', + ) + ) + ) + ->willThrowException( + new \Composer\Downloader\TransportException( + sprintf( + 'The \'%s\' URL could not be accessed: HTTP/1.1 400 BAD REQUEST', + Bitbucket::OAUTH2_ACCESS_TOKEN_URL + ), + 400 + ) + ); + + $this->assertEquals(array(), $this->bitbucket->requestToken($this->origin, $this->username, $this->password)); + } + public function testUsernamePasswordAuthenticationFlow() { - $io = $this->getIOMock(); - $io + $this->io ->expects($this->at(0)) ->method('writeError') ->with($this->message) ; - $io->expects($this->exactly(2)) + $this->io->expects($this->exactly(2)) ->method('askAndHideAnswer') ->withConsecutive( array('Consumer Key (hidden): '), array('Consumer Secret (hidden): ') ) - ->willReturnOnConsecutiveCalls($this->username, $this->password); + ->willReturnOnConsecutiveCalls($this->consumer_key, $this->consumer_secret); - $rfs = $this->getRemoteFilesystemMock(); - $rfs + $this->rfs ->expects($this->once()) ->method('getContents') ->with( @@ -56,50 +164,18 @@ class BitbucketTest extends \PHPUnit_Framework_TestCase ->willReturn(sprintf('{}', $this->token)) ; - $config = $this->getConfigMock(); - $config + $this->config ->expects($this->exactly(2)) ->method('getAuthConfigSource') ->willReturn($this->getAuthJsonMock()) ; - $config + $this->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; + $this->assertTrue($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); } private function getAuthJsonMock() From ccfd9018b86a3d3bb759c60e8453b9b9271464d0 Mon Sep 17 00:00:00 2001 From: Stefan Grootscholten Date: Sat, 11 Jun 2016 17:25:59 +0200 Subject: [PATCH 11/12] Add unit tests for the GitBitbucketDriver class. --- .../Repository/Vcs/GitBitbucketDriverTest.php | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php diff --git a/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php new file mode 100644 index 000000000..b2f01e557 --- /dev/null +++ b/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php @@ -0,0 +1,218 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository\Vcs; + +use Composer\Config; +use Composer\TestCase; +use Composer\Util\Filesystem; + +class GitBitbucketDriverTest extends TestCase +{ + /** @type \Composer\IO\IOInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $io; + /** @type \Composer\Config */ + private $config; + /** @type \Composer\Util\RemoteFilesystem|\PHPUnit_Framework_MockObject_MockObject */ + private $rfs; + /** @type string */ + private $home; + /** @type string */ + private $originUrl = 'bitbucket.org'; + + protected function setUp() + { + $this->io = $this->getMock('Composer\IO\IOInterface'); + + $this->home = $this->getUniqueTmpDirectory(); + + $this->config = new Config(); + $this->config->merge(array( + 'config' => array( + 'home' => $this->home, + ), + )); + + $this->rfs = $this->getMockBuilder('Composer\Util\RemoteFilesystem') + ->disableOriginalConstructor() + ->getMock(); + } + + public function tearDown() + { + $fs = new Filesystem; + $fs->removeDirectory($this->home); + } + + /** + * @param array $repoConfig + * @return GitBitbucketDriver + */ + private function getDriver(array $repoConfig) + { + $driver = new GitBitbucketDriver( + $repoConfig, + $this->io, + $this->config, + null, + $this->rfs + ); + + $driver->initialize(); + + return $driver; + } + + public function testGetRootIdentifier() + { + $driver = $this->getDriver(array('url' => 'https://bitbucket.org/user/repo.git')); + + $this->rfs->expects($this->any()) + ->method('getContents') + ->with( + $this->originUrl, + 'https://api.bitbucket.org/1.0/repositories/user/repo', + false + ) + ->willReturn( + '{"scm": "git", "has_wiki": false, "last_updated": "2016-05-17T13:20:21.993", "no_forks": true, "forks_count": 0, "created_on": "2015-02-18T16:22:24.688", "owner": "user", "logo": "https://bitbucket.org/user/repo/avatar/32/?ts=1463484021", "email_mailinglist": "", "is_mq": false, "size": 9975494, "read_only": false, "fork_of": null, "mq_of": null, "followers_count": 0, "state": "available", "utc_created_on": "2015-02-18 15:22:24+00:00", "website": "", "description": "", "has_issues": false, "is_fork": false, "slug": "repo", "is_private": true, "name": "repo", "language": "php", "utc_last_updated": "2016-05-17 11:20:21+00:00", "no_public_forks": true, "creator": null, "resource_uri": "/1.0/repositories/user/repo"}' + ); + + $this->assertEquals( + 'master', + $driver->getRootIdentifier() + ); + } + + public function testGetParams() + { + $url = 'https://bitbucket.org/user/repo.git'; + $driver = $this->getDriver(array('url' => $url)); + + $this->assertEquals($url, $driver->getUrl()); + + $this->assertEquals( + array( + 'type' => 'zip', + 'url' => 'https://bitbucket.org/user/repo/get/reference.zip', + 'reference' => 'reference', + 'shasum' => '' + ), + $driver->getDist('reference') + ); + + $this->assertEquals( + array('type' => 'git', 'url' => $url, 'reference' => 'reference'), + $driver->getSource('reference') + ); + } + + public function testGetComposerInformation() + { + $driver = $this->getDriver(array('url' => 'https://bitbucket.org/user/repo.git')); + + $this->rfs->expects($this->any()) + ->method('getContents') + ->withConsecutive( + array('bitbucket.org', 'https://api.bitbucket.org/1.0/repositories/user/repo/src/master/composer.json', false), + array('bitbucket.org', 'https://api.bitbucket.org/1.0/repositories/user/repo/changesets/master', false) + ) + ->willReturnOnConsecutiveCalls( + '{"node": "937992d19d72", "path": "composer.json", "data": "{\n \"name\": \"user/repo\",\n \"description\": \"test repo\",\n \"license\": \"GPL\",\n \"authors\": [\n {\n \"name\": \"Name\",\n \"email\": \"local@domain.tld\"\n }\n ],\n \"require\": {\n \"creator/package\": \"^1.0\"\n },\n \"require-dev\": {\n \"phpunit/phpunit\": \"~4.8\"\n }\n}\n", "size": 269}', + '{"node": "937992d19d72", "files": [{"type": "modified", "file": "path/to/file"}], "raw_author": "User ", "utctimestamp": "2016-05-17 11:19:52+00:00", "author": "user", "timestamp": "2016-05-17 13:19:52", "raw_node": "937992d19d72b5116c3e8c4a04f960e5fa270b22", "parents": ["71e195a33361"], "branch": "master", "message": "Commit message\n", "revision": null, "size": -1}' + ); + + $this->assertEquals( + array( + 'name' => 'user/repo', + 'description' => 'test repo', + 'license' => 'GPL', + 'authors' => array( + array( + 'name' => 'Name', + 'email' => 'local@domain.tld' + ) + ), + 'require' => array( + 'creator/package' => '^1.0' + ), + 'require-dev' => array( + 'phpunit/phpunit' => '~4.8' + ), + 'time' => '2016-05-17 13:19:52', + ), + $driver->getComposerInformation('master') + ); + } + + public function testGetTags() + { + $driver = $this->getDriver(array('url' => 'https://bitbucket.org/user/repo.git')); + + $this->rfs->expects($this->once()) + ->method('getContents') + ->with( + 'bitbucket.org', + 'https://api.bitbucket.org/1.0/repositories/user/repo/tags', + false + ) + ->willReturn( + '{"1.0.1": {"node": "9b78a3932143", "files": [{"type": "modified", "file": "path/to/file"}], "branches": [], "raw_author": "User ", "utctimestamp": "2015-04-16 14:50:40+00:00", "author": "user", "timestamp": "2015-04-16 16:50:40", "raw_node": "9b78a3932143497c519e49b8241083838c8ff8a1", "parents": ["84531c04dbfc", "50c2a4635ad0"], "branch": null, "message": "Commit message\n", "revision": null, "size": -1}, "1.0.0": {"node": "d3393d514318", "files": [{"type": "modified", "file": "path/to/file2"}], "branches": [], "raw_author": "User ", "utctimestamp": "2015-04-16 09:31:45+00:00", "author": "user", "timestamp": "2015-04-16 11:31:45", "raw_node": "d3393d514318a9267d2f8ebbf463a9aaa389f8eb", "parents": ["5a29a73cd1a0"], "branch": null, "message": "Commit message\n", "revision": null, "size": -1}}' + ); + + $this->assertEquals( + array( + '1.0.1' => '9b78a3932143497c519e49b8241083838c8ff8a1', + '1.0.0' => 'd3393d514318a9267d2f8ebbf463a9aaa389f8eb' + ), + $driver->getTags() + ); + } + + public function testGetBranches() + { + $driver = $this->getDriver(array('url' => 'https://bitbucket.org/user/repo.git')); + + $this->rfs->expects($this->once()) + ->method('getContents') + ->with( + 'bitbucket.org', + 'https://api.bitbucket.org/1.0/repositories/user/repo/branches', + false + ) + ->willReturn( + '{"master": {"node": "937992d19d72", "files": [{"type": "modified", "file": "path/to/file"}], "raw_author": "User ", "utctimestamp": "2016-05-17 11:19:52+00:00", "author": "user", "timestamp": "2016-05-17 13:19:52", "raw_node": "937992d19d72b5116c3e8c4a04f960e5fa270b22", "parents": ["71e195a33361"], "branch": "master", "message": "Commit message\n", "revision": null, "size": -1}}' + ); + + $this->assertEquals( + array( + 'master' => '937992d19d72b5116c3e8c4a04f960e5fa270b22' + ), + $driver->getBranches() + ); + } + + public function testSupports() + { + $this->assertTrue( + GitBitbucketDriver::supports($this->io, $this->config, 'https://bitbucket.org/user/repo.git') + ); + + $this->assertFalse( + GitBitbucketDriver::supports($this->io, $this->config, 'git@bitbucket.org:user/repo.git') + ); + + $this->assertFalse( + GitBitbucketDriver::supports($this->io, $this->config, 'https://github.com/user/repo.git') + ); + } +} From 78fcb5a366a260820d01389f034b2d9f911d0198 Mon Sep 17 00:00:00 2001 From: Stefan Grootscholten Date: Fri, 17 Jun 2016 17:35:43 +0200 Subject: [PATCH 12/12] Add support section to composer data for bitbucket repositories. --- .../Repository/Vcs/GitBitbucketDriver.php | 27 +++++++++++++++++++ .../Repository/Vcs/GitBitbucketDriverTest.php | 14 ++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/Composer/Repository/Vcs/GitBitbucketDriver.php b/src/Composer/Repository/Vcs/GitBitbucketDriver.php index 4ac356630..72af4183e 100644 --- a/src/Composer/Repository/Vcs/GitBitbucketDriver.php +++ b/src/Composer/Repository/Vcs/GitBitbucketDriver.php @@ -34,6 +34,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface protected $branches; protected $rootIdentifier; protected $infoCache = array(); + private $hasIssues; /** * @var GitDriver */ @@ -63,6 +64,7 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface if (null === $this->rootIdentifier) { $resource = $this->getScheme() . '://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository; $repoData = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource, true), $resource); + $this->hasIssues = !empty($repoData['has_issues']); $this->rootIdentifier = !empty($repoData['main_branch']) ? $repoData['main_branch'] : 'master'; } @@ -130,6 +132,31 @@ class GitBitbucketDriver extends VcsDriver implements VcsDriverInterface $changeset = JsonFile::parseJson($this->getContentsWithOAuthCredentials($resource), $resource); $composer['time'] = $changeset['timestamp']; } + if (!isset($composer['support']['source'])) { + $label = array_search($identifier, $this->getTags()) ?: array_search($identifier, $this->getBranches()) ?: $identifier; + + if (array_key_exists($label, $tags = $this->getTags())) { + $hash = $tags[$label]; + } elseif (array_key_exists($label, $branches = $this->getBranches())) { + $hash = $branches[$label]; + } + + if (! isset($hash)) { + $composer['support']['source'] = sprintf('https://%s/%s/%s/src', $this->originUrl, $this->owner, $this->repository); + } else { + $composer['support']['source'] = sprintf( + 'https://%s/%s/%s/src/%s/?at=%s', + $this->originUrl, + $this->owner, + $this->repository, + $hash, + $label + ); + } + } + if (!isset($composer['support']['issues']) && $this->hasIssues) { + $composer['support']['issues'] = sprintf('https://%s/%s/%s/issues', $this->originUrl, $this->owner, $this->repository); + } if (preg_match('{[a-f0-9]{40}}i', $identifier)) { $this->cache->write($identifier, json_encode($composer)); diff --git a/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php index b2f01e557..b8a79fea7 100644 --- a/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php @@ -16,6 +16,9 @@ use Composer\Config; use Composer\TestCase; use Composer\Util\Filesystem; +/** + * @group bitbucket + */ class GitBitbucketDriverTest extends TestCase { /** @type \Composer\IO\IOInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -124,11 +127,15 @@ class GitBitbucketDriverTest extends TestCase ->method('getContents') ->withConsecutive( array('bitbucket.org', 'https://api.bitbucket.org/1.0/repositories/user/repo/src/master/composer.json', false), - array('bitbucket.org', 'https://api.bitbucket.org/1.0/repositories/user/repo/changesets/master', false) + array('bitbucket.org', 'https://api.bitbucket.org/1.0/repositories/user/repo/changesets/master', false), + array('bitbucket.org', 'https://api.bitbucket.org/1.0/repositories/user/repo/tags', false), + array('bitbucket.org', 'https://api.bitbucket.org/1.0/repositories/user/repo/branches', false) ) ->willReturnOnConsecutiveCalls( '{"node": "937992d19d72", "path": "composer.json", "data": "{\n \"name\": \"user/repo\",\n \"description\": \"test repo\",\n \"license\": \"GPL\",\n \"authors\": [\n {\n \"name\": \"Name\",\n \"email\": \"local@domain.tld\"\n }\n ],\n \"require\": {\n \"creator/package\": \"^1.0\"\n },\n \"require-dev\": {\n \"phpunit/phpunit\": \"~4.8\"\n }\n}\n", "size": 269}', - '{"node": "937992d19d72", "files": [{"type": "modified", "file": "path/to/file"}], "raw_author": "User ", "utctimestamp": "2016-05-17 11:19:52+00:00", "author": "user", "timestamp": "2016-05-17 13:19:52", "raw_node": "937992d19d72b5116c3e8c4a04f960e5fa270b22", "parents": ["71e195a33361"], "branch": "master", "message": "Commit message\n", "revision": null, "size": -1}' + '{"node": "937992d19d72", "files": [{"type": "modified", "file": "path/to/file"}], "raw_author": "User ", "utctimestamp": "2016-05-17 11:19:52+00:00", "author": "user", "timestamp": "2016-05-17 13:19:52", "raw_node": "937992d19d72b5116c3e8c4a04f960e5fa270b22", "parents": ["71e195a33361"], "branch": "master", "message": "Commit message\n", "revision": null, "size": -1}', + '{}', + '{"master": {"node": "937992d19d72", "files": [{"type": "modified", "file": "path/to/file"}], "raw_author": "User ", "utctimestamp": "2016-05-17 11:19:52+00:00", "author": "user", "timestamp": "2016-05-17 13:19:52", "raw_node": "937992d19d72b5116c3e8c4a04f960e5fa270b22", "parents": ["71e195a33361"], "branch": "master", "message": "Commit message\n", "revision": null, "size": -1}}' ); $this->assertEquals( @@ -149,6 +156,9 @@ class GitBitbucketDriverTest extends TestCase 'phpunit/phpunit' => '~4.8' ), 'time' => '2016-05-17 13:19:52', + 'support' => array( + 'source' => 'https://bitbucket.org/user/repo/src/937992d19d72b5116c3e8c4a04f960e5fa270b22/?at=master' + ) ), $driver->getComposerInformation('master') );