Bitbucket.php 8.3 KB

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