Bitbucket.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\Util;
  12. use Composer\Factory;
  13. use Composer\IO\IOInterface;
  14. use Composer\Config;
  15. use Composer\Downloader\TransportException;
  16. /**
  17. * @author Paul Wenke <wenke.paul@gmail.com>
  18. */
  19. class Bitbucket
  20. {
  21. /** @var IOInterface */
  22. private $io;
  23. /** @var Config */
  24. private $config;
  25. /** @var ProcessExecutor */
  26. private $process;
  27. /** @var HttpDownloader */
  28. private $httpDownloader;
  29. /** @var array */
  30. private $token = array();
  31. /** @var int|null */
  32. private $time;
  33. const OAUTH2_ACCESS_TOKEN_URL = 'https://bitbucket.org/site/oauth2/access_token';
  34. /**
  35. * Constructor.
  36. *
  37. * @param IOInterface $io The IO instance
  38. * @param Config $config The composer configuration
  39. * @param ProcessExecutor $process Process instance, injectable for mocking
  40. * @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking
  41. * @param int $time Timestamp, injectable for mocking
  42. */
  43. public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, HttpDownloader $httpDownloader = null, $time = null)
  44. {
  45. $this->io = $io;
  46. $this->config = $config;
  47. $this->process = $process ?: new ProcessExecutor($io);
  48. $this->httpDownloader = $httpDownloader ?: Factory::createHttpDownloader($this->io, $config);
  49. $this->time = $time;
  50. }
  51. /**
  52. * @return string
  53. */
  54. public function getToken()
  55. {
  56. if (!isset($this->token['access_token'])) {
  57. return '';
  58. }
  59. return $this->token['access_token'];
  60. }
  61. /**
  62. * Attempts to authorize a Bitbucket domain via OAuth
  63. *
  64. * @param string $originUrl The host this Bitbucket instance is located at
  65. * @return bool true on success
  66. */
  67. public function authorizeOAuth($originUrl)
  68. {
  69. if ($originUrl !== 'bitbucket.org') {
  70. return false;
  71. }
  72. // if available use token from git config
  73. if (0 === $this->process->execute('git config bitbucket.accesstoken', $output)) {
  74. $this->io->setAuthentication($originUrl, 'x-token-auth', trim($output));
  75. return true;
  76. }
  77. return false;
  78. }
  79. /**
  80. * @param string $originUrl
  81. * @return bool
  82. */
  83. private function requestAccessToken($originUrl)
  84. {
  85. try {
  86. $response = $this->httpDownloader->get(self::OAUTH2_ACCESS_TOKEN_URL, array(
  87. 'retry-auth-failure' => false,
  88. 'http' => array(
  89. 'method' => 'POST',
  90. 'content' => 'grant_type=client_credentials',
  91. ),
  92. ));
  93. $this->token = $response->decodeJson();
  94. } catch (TransportException $e) {
  95. if ($e->getCode() === 400) {
  96. $this->io->writeError('<error>Invalid OAuth consumer provided.</error>');
  97. $this->io->writeError('This can have two reasons:');
  98. $this->io->writeError('1. You are authenticating with a bitbucket username/password combination');
  99. $this->io->writeError('2. You are using an OAuth consumer, but didn\'t configure a (dummy) callback url');
  100. return false;
  101. } elseif (in_array($e->getCode(), array(403, 401))) {
  102. $this->io->writeError('<error>Invalid OAuth consumer provided.</error>');
  103. $this->io->writeError('You can also add it manually later by using "composer config --global --auth bitbucket-oauth.bitbucket.org <consumer-key> <consumer-secret>"');
  104. return false;
  105. }
  106. throw $e;
  107. }
  108. return true;
  109. }
  110. /**
  111. * Authorizes a Bitbucket domain interactively via OAuth
  112. *
  113. * @param string $originUrl The host this Bitbucket instance is located at
  114. * @param string $message The reason this authorization is required
  115. * @throws \RuntimeException
  116. * @throws TransportException|\Exception
  117. * @return bool true on success
  118. */
  119. public function authorizeOAuthInteractively($originUrl, $message = null)
  120. {
  121. if ($message) {
  122. $this->io->writeError($message);
  123. }
  124. $url = 'https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html';
  125. $this->io->writeError(sprintf('Follow the instructions on %s', $url));
  126. $this->io->writeError(sprintf('to create a consumer. It will be stored in "%s" for future use by Composer.', $this->config->getAuthConfigSource()->getName()));
  127. $this->io->writeError('Ensure you enter a "Callback URL" (http://example.com is fine) or it will not be possible to create an Access Token (this callback url will not be used by composer)');
  128. $consumerKey = trim($this->io->askAndHideAnswer('Consumer Key (hidden): '));
  129. if (!$consumerKey) {
  130. $this->io->writeError('<warning>No consumer key given, aborting.</warning>');
  131. $this->io->writeError('You can also add it manually later by using "composer config --global --auth bitbucket-oauth.bitbucket.org <consumer-key> <consumer-secret>"');
  132. return false;
  133. }
  134. $consumerSecret = trim($this->io->askAndHideAnswer('Consumer Secret (hidden): '));
  135. if (!$consumerSecret) {
  136. $this->io->writeError('<warning>No consumer secret given, aborting.</warning>');
  137. $this->io->writeError('You can also add it manually later by using "composer config --global --auth bitbucket-oauth.bitbucket.org <consumer-key> <consumer-secret>"');
  138. return false;
  139. }
  140. $this->io->setAuthentication($originUrl, $consumerKey, $consumerSecret);
  141. if (!$this->requestAccessToken($originUrl)) {
  142. return false;
  143. }
  144. // store value in user config
  145. $this->storeInAuthConfig($originUrl, $consumerKey, $consumerSecret);
  146. // Remove conflicting basic auth credentials (if available)
  147. $this->config->getAuthConfigSource()->removeConfigSetting('http-basic.' . $originUrl);
  148. $this->io->writeError('<info>Consumer stored successfully.</info>');
  149. return true;
  150. }
  151. /**
  152. * Retrieves an access token from Bitbucket.
  153. *
  154. * @param string $originUrl
  155. * @param string $consumerKey
  156. * @param string $consumerSecret
  157. * @return string
  158. */
  159. public function requestToken($originUrl, $consumerKey, $consumerSecret)
  160. {
  161. if (!empty($this->token) || $this->getTokenFromConfig($originUrl)) {
  162. return $this->token['access_token'];
  163. }
  164. $this->io->setAuthentication($originUrl, $consumerKey, $consumerSecret);
  165. if (!$this->requestAccessToken($originUrl)) {
  166. return '';
  167. }
  168. $this->storeInAuthConfig($originUrl, $consumerKey, $consumerSecret);
  169. return $this->token['access_token'];
  170. }
  171. /**
  172. * Store the new/updated credentials to the configuration
  173. * @param string $originUrl
  174. * @param string $consumerKey
  175. * @param string $consumerSecret
  176. */
  177. private function storeInAuthConfig($originUrl, $consumerKey, $consumerSecret)
  178. {
  179. $this->config->getConfigSource()->removeConfigSetting('bitbucket-oauth.'.$originUrl);
  180. $time = null === $this->time ? time() : $this->time;
  181. $consumer = array(
  182. "consumer-key" => $consumerKey,
  183. "consumer-secret" => $consumerSecret,
  184. "access-token" => $this->token['access_token'],
  185. "access-token-expiration" => $time + $this->token['expires_in'],
  186. );
  187. $this->config->getAuthConfigSource()->addConfigSetting('bitbucket-oauth.'.$originUrl, $consumer);
  188. }
  189. /**
  190. * @param string $originUrl
  191. * @return bool
  192. */
  193. private function getTokenFromConfig($originUrl)
  194. {
  195. $authConfig = $this->config->get('bitbucket-oauth');
  196. if (
  197. !isset($authConfig[$originUrl]['access-token'])
  198. || !isset($authConfig[$originUrl]['access-token-expiration'])
  199. || time() > $authConfig[$originUrl]['access-token-expiration']
  200. ) {
  201. return false;
  202. }
  203. $this->token = array(
  204. 'access_token' => $authConfig[$originUrl]['access-token'],
  205. );
  206. return true;
  207. }
  208. }