GitHub.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  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 Jordi Boggiano <j.boggiano@seld.be>
  18. */
  19. class GitHub
  20. {
  21. /** @var IOInterface */
  22. protected $io;
  23. /** @var Config */
  24. protected $config;
  25. /** @var ProcessExecutor */
  26. protected $process;
  27. /** @var HttpDownloader */
  28. protected $httpDownloader;
  29. /**
  30. * Constructor.
  31. *
  32. * @param IOInterface $io The IO instance
  33. * @param Config $config The composer configuration
  34. * @param ProcessExecutor $process Process instance, injectable for mocking
  35. * @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking
  36. */
  37. public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, HttpDownloader $httpDownloader = null)
  38. {
  39. $this->io = $io;
  40. $this->config = $config;
  41. $this->process = $process ?: new ProcessExecutor($io);
  42. $this->httpDownloader = $httpDownloader ?: Factory::createHttpDownloader($this->io, $config);
  43. }
  44. /**
  45. * Attempts to authorize a GitHub domain via OAuth
  46. *
  47. * @param string $originUrl The host this GitHub instance is located at
  48. * @return bool true on success
  49. */
  50. public function authorizeOAuth($originUrl)
  51. {
  52. if (!in_array($originUrl, $this->config->get('github-domains'))) {
  53. return false;
  54. }
  55. // if available use token from git config
  56. if (0 === $this->process->execute('git config github.accesstoken', $output)) {
  57. $this->io->setAuthentication($originUrl, trim($output), 'x-oauth-basic');
  58. return true;
  59. }
  60. return false;
  61. }
  62. /**
  63. * Authorizes a GitHub domain interactively via OAuth
  64. *
  65. * @param string $originUrl The host this GitHub instance is located at
  66. * @param string $message The reason this authorization is required
  67. * @throws \RuntimeException
  68. * @throws TransportException|\Exception
  69. * @return bool true on success
  70. */
  71. public function authorizeOAuthInteractively($originUrl, $message = null)
  72. {
  73. if ($message) {
  74. $this->io->writeError($message);
  75. }
  76. $note = 'Composer';
  77. if ($this->config->get('github-expose-hostname') === true && 0 === $this->process->execute('hostname', $output)) {
  78. $note .= ' on ' . trim($output);
  79. }
  80. $note .= ' ' . date('Y-m-d Hi');
  81. $url = 'https://'.$originUrl.'/settings/tokens/new?scopes=repo&description=' . str_replace('%20', '+', rawurlencode($note));
  82. $this->io->writeError(sprintf('Head to %s', $url));
  83. $this->io->writeError(sprintf('to retrieve a token. It will be stored in "%s" for future use by Composer.', $this->config->getAuthConfigSource()->getName()));
  84. $token = trim($this->io->askAndHideAnswer('Token (hidden): '));
  85. if (!$token) {
  86. $this->io->writeError('<warning>No token given, aborting.</warning>');
  87. $this->io->writeError('You can also add it manually later by using "composer config --global --auth github-oauth.github.com <token>"');
  88. return false;
  89. }
  90. $this->io->setAuthentication($originUrl, $token, 'x-oauth-basic');
  91. try {
  92. $apiUrl = ('github.com' === $originUrl) ? 'api.github.com/' : $originUrl . '/api/v3/';
  93. $this->httpDownloader->get('https://'. $apiUrl, array(
  94. 'retry-auth-failure' => false,
  95. ));
  96. } catch (TransportException $e) {
  97. if (in_array($e->getCode(), array(403, 401))) {
  98. $this->io->writeError('<error>Invalid token provided.</error>');
  99. $this->io->writeError('You can also add it manually later by using "composer config --global --auth github-oauth.github.com <token>"');
  100. return false;
  101. }
  102. throw $e;
  103. }
  104. // store value in user config
  105. $this->config->getConfigSource()->removeConfigSetting('github-oauth.'.$originUrl);
  106. $this->config->getAuthConfigSource()->addConfigSetting('github-oauth.'.$originUrl, $token);
  107. $this->io->writeError('<info>Token stored successfully.</info>');
  108. return true;
  109. }
  110. /**
  111. * Extract ratelimit from response.
  112. *
  113. * @param array $headers Headers from Composer\Downloader\TransportException.
  114. *
  115. * @return array Associative array with the keys limit and reset.
  116. */
  117. public function getRateLimit(array $headers)
  118. {
  119. $rateLimit = array(
  120. 'limit' => '?',
  121. 'reset' => '?',
  122. );
  123. foreach ($headers as $header) {
  124. $header = trim($header);
  125. if (false === strpos($header, 'X-RateLimit-')) {
  126. continue;
  127. }
  128. list($type, $value) = explode(':', $header, 2);
  129. switch ($type) {
  130. case 'X-RateLimit-Limit':
  131. $rateLimit['limit'] = (int) trim($value);
  132. break;
  133. case 'X-RateLimit-Reset':
  134. $rateLimit['reset'] = date('Y-m-d H:i:s', (int) trim($value));
  135. break;
  136. }
  137. }
  138. return $rateLimit;
  139. }
  140. /**
  141. * Finds whether a request failed due to rate limiting
  142. *
  143. * @param array $headers Headers from Composer\Downloader\TransportException.
  144. *
  145. * @return bool
  146. */
  147. public function isRateLimited(array $headers)
  148. {
  149. foreach ($headers as $header) {
  150. if (preg_match('{^X-RateLimit-Remaining: *0$}i', trim($header))) {
  151. return true;
  152. }
  153. }
  154. return false;
  155. }
  156. }