Svn.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  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\Config;
  13. use Composer\Downloader\TransportException;
  14. use Composer\IO\IOInterface;
  15. /**
  16. * @author Till Klampaeckel <till@php.net>
  17. * @author Jordi Boggiano <j.boggiano@seld.be>
  18. */
  19. class Svn
  20. {
  21. const MAX_QTY_AUTH_TRIES = 5;
  22. /**
  23. * @var array
  24. */
  25. protected $credentials;
  26. /**
  27. * @var bool
  28. */
  29. protected $hasAuth;
  30. /**
  31. * @var \Composer\IO\IOInterface
  32. */
  33. protected $io;
  34. /**
  35. * @var string
  36. */
  37. protected $url;
  38. /**
  39. * @var bool
  40. */
  41. protected $cacheCredentials = true;
  42. /**
  43. * @var ProcessExecutor
  44. */
  45. protected $process;
  46. /**
  47. * @var int
  48. */
  49. protected $qtyAuthTries = 0;
  50. /**
  51. * @var \Composer\Config
  52. */
  53. protected $config;
  54. /**
  55. * @param string $url
  56. * @param \Composer\IO\IOInterface $io
  57. * @param Config $config
  58. * @param ProcessExecutor $process
  59. */
  60. public function __construct($url, IOInterface $io, Config $config, ProcessExecutor $process = null)
  61. {
  62. $this->url = $url;
  63. $this->io = $io;
  64. $this->config = $config;
  65. $this->process = $process ?: new ProcessExecutor;
  66. }
  67. public static function cleanEnv()
  68. {
  69. // clean up env for OSX, see https://github.com/composer/composer/issues/2146#issuecomment-35478940
  70. putenv("DYLD_LIBRARY_PATH");
  71. unset($_SERVER['DYLD_LIBRARY_PATH']);
  72. }
  73. /**
  74. * Execute an SVN command and try to fix up the process with credentials
  75. * if necessary.
  76. *
  77. * @param string $command SVN command to run
  78. * @param string $url SVN url
  79. * @param string $cwd Working directory
  80. * @param string $path Target for a checkout
  81. * @param bool $verbose Output all output to the user
  82. *
  83. * @throws \RuntimeException
  84. * @return string
  85. */
  86. public function execute($command, $url, $cwd = null, $path = null, $verbose = false)
  87. {
  88. // Ensure we are allowed to use this URL by config
  89. $this->config->prohibitUrlByConfig($url);
  90. $svnCommand = $this->getCommand($command, $url, $path);
  91. $output = null;
  92. $io = $this->io;
  93. $handler = function ($type, $buffer) use (&$output, $io, $verbose) {
  94. if ($type !== 'out') {
  95. return;
  96. }
  97. if ('Redirecting to URL ' === substr($buffer, 0, 19)) {
  98. return;
  99. }
  100. $output .= $buffer;
  101. if ($verbose) {
  102. $io->writeError($buffer, false);
  103. }
  104. };
  105. $status = $this->process->execute($svnCommand, $handler, $cwd);
  106. if (0 === $status) {
  107. return $output;
  108. }
  109. $errorOutput = $this->process->getErrorOutput();
  110. $fullOutput = implode("\n", array($output, $errorOutput));
  111. // the error is not auth-related
  112. if (false === stripos($fullOutput, 'Could not authenticate to server:')
  113. && false === stripos($fullOutput, 'authorization failed')
  114. && false === stripos($fullOutput, 'svn: E170001:')
  115. && false === stripos($fullOutput, 'svn: E215004:')) {
  116. throw new \RuntimeException($fullOutput);
  117. }
  118. if (!$this->hasAuth()) {
  119. $this->doAuthDance();
  120. }
  121. // try to authenticate if maximum quantity of tries not reached
  122. if ($this->qtyAuthTries++ < self::MAX_QTY_AUTH_TRIES) {
  123. // restart the process
  124. return $this->execute($command, $url, $cwd, $path, $verbose);
  125. }
  126. throw new \RuntimeException(
  127. 'wrong credentials provided ('.$fullOutput.')'
  128. );
  129. }
  130. /**
  131. * @param bool $cacheCredentials
  132. */
  133. public function setCacheCredentials($cacheCredentials)
  134. {
  135. $this->cacheCredentials = $cacheCredentials;
  136. }
  137. /**
  138. * Repositories requests credentials, let's put them in.
  139. *
  140. * @throws \RuntimeException
  141. * @return \Composer\Util\Svn
  142. */
  143. protected function doAuthDance()
  144. {
  145. // cannot ask for credentials in non interactive mode
  146. if (!$this->io->isInteractive()) {
  147. throw new \RuntimeException(
  148. 'can not ask for authentication in non interactive mode'
  149. );
  150. }
  151. $this->io->writeError("The Subversion server ({$this->url}) requested credentials:");
  152. $this->hasAuth = true;
  153. $this->credentials['username'] = $this->io->ask("Username: ");
  154. $this->credentials['password'] = $this->io->askAndHideAnswer("Password: ");
  155. $this->cacheCredentials = $this->io->askConfirmation("Should Subversion cache these credentials? (yes/no) ", true);
  156. return $this;
  157. }
  158. /**
  159. * A method to create the svn commands run.
  160. *
  161. * @param string $cmd Usually 'svn ls' or something like that.
  162. * @param string $url Repo URL.
  163. * @param string $path Target for a checkout
  164. *
  165. * @return string
  166. */
  167. protected function getCommand($cmd, $url, $path = null)
  168. {
  169. $cmd = sprintf('%s %s%s %s',
  170. $cmd,
  171. '--non-interactive ',
  172. $this->getCredentialString(),
  173. ProcessExecutor::escape($url)
  174. );
  175. if ($path) {
  176. $cmd .= ' ' . ProcessExecutor::escape($path);
  177. }
  178. return $cmd;
  179. }
  180. /**
  181. * Return the credential string for the svn command.
  182. *
  183. * Adds --no-auth-cache when credentials are present.
  184. *
  185. * @return string
  186. */
  187. protected function getCredentialString()
  188. {
  189. if (!$this->hasAuth()) {
  190. return '';
  191. }
  192. return sprintf(
  193. ' %s--username %s --password %s ',
  194. $this->getAuthCache(),
  195. ProcessExecutor::escape($this->getUsername()),
  196. ProcessExecutor::escape($this->getPassword())
  197. );
  198. }
  199. /**
  200. * Get the password for the svn command. Can be empty.
  201. *
  202. * @throws \LogicException
  203. * @return string
  204. */
  205. protected function getPassword()
  206. {
  207. if ($this->credentials === null) {
  208. throw new \LogicException("No svn auth detected.");
  209. }
  210. return isset($this->credentials['password']) ? $this->credentials['password'] : '';
  211. }
  212. /**
  213. * Get the username for the svn command.
  214. *
  215. * @throws \LogicException
  216. * @return string
  217. */
  218. protected function getUsername()
  219. {
  220. if ($this->credentials === null) {
  221. throw new \LogicException("No svn auth detected.");
  222. }
  223. return $this->credentials['username'];
  224. }
  225. /**
  226. * Detect Svn Auth.
  227. *
  228. * @return bool
  229. */
  230. protected function hasAuth()
  231. {
  232. if (null !== $this->hasAuth) {
  233. return $this->hasAuth;
  234. }
  235. if (false === $this->createAuthFromConfig()) {
  236. $this->createAuthFromUrl();
  237. }
  238. return $this->hasAuth;
  239. }
  240. /**
  241. * Return the no-auth-cache switch.
  242. *
  243. * @return string
  244. */
  245. protected function getAuthCache()
  246. {
  247. return $this->cacheCredentials ? '' : '--no-auth-cache ';
  248. }
  249. /**
  250. * Create the auth params from the configuration file.
  251. *
  252. * @return bool
  253. */
  254. private function createAuthFromConfig()
  255. {
  256. if (!$this->config->has('http-basic')) {
  257. return $this->hasAuth = false;
  258. }
  259. $authConfig = $this->config->get('http-basic');
  260. $host = parse_url($this->url, PHP_URL_HOST);
  261. if (isset($authConfig[$host])) {
  262. $this->credentials['username'] = $authConfig[$host]['username'];
  263. $this->credentials['password'] = $authConfig[$host]['password'];
  264. return $this->hasAuth = true;
  265. }
  266. return $this->hasAuth = false;
  267. }
  268. /**
  269. * Create the auth params from the url
  270. *
  271. * @return bool
  272. */
  273. private function createAuthFromUrl()
  274. {
  275. $uri = parse_url($this->url);
  276. if (empty($uri['user'])) {
  277. return $this->hasAuth = false;
  278. }
  279. $this->credentials['username'] = $uri['user'];
  280. if (!empty($uri['pass'])) {
  281. $this->credentials['password'] = $uri['pass'];
  282. }
  283. return $this->hasAuth = true;
  284. }
  285. }