HttpDownloader.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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\IO\IOInterface;
  14. use Composer\Downloader\TransportException;
  15. use Composer\CaBundle\CaBundle;
  16. use Composer\Util\Http\Response;
  17. use Composer\Composer;
  18. use Composer\Package\Version\VersionParser;
  19. use Composer\Semver\Constraint\Constraint;
  20. use React\Promise\Promise;
  21. /**
  22. * @author Jordi Boggiano <j.boggiano@seld.be>
  23. */
  24. class HttpDownloader
  25. {
  26. const STATUS_QUEUED = 1;
  27. const STATUS_STARTED = 2;
  28. const STATUS_COMPLETED = 3;
  29. const STATUS_FAILED = 4;
  30. private $io;
  31. private $config;
  32. private $jobs = array();
  33. private $options = array();
  34. private $runningJobs = 0;
  35. private $maxJobs = 10;
  36. private $lastProgress;
  37. private $disableTls = false;
  38. private $curl;
  39. private $rfs;
  40. private $idGen = 0;
  41. private $disabled;
  42. /**
  43. * @param IOInterface $io The IO instance
  44. * @param Config $config The config
  45. * @param array $options The options
  46. * @param bool $disableTls
  47. */
  48. public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false)
  49. {
  50. $this->io = $io;
  51. $this->disabled = (bool) getenv('COMPOSER_DISABLE_NETWORK');
  52. // Setup TLS options
  53. // The cafile option can be set via config.json
  54. if ($disableTls === false) {
  55. $this->options = StreamContextFactory::getTlsDefaults($options, $io);
  56. } else {
  57. $this->disableTls = true;
  58. }
  59. // handle the other externally set options normally.
  60. $this->options = array_replace_recursive($this->options, $options);
  61. $this->config = $config;
  62. // TODO enable curl only on 5.6+ if older versions cause any problem
  63. if (extension_loaded('curl')) {
  64. $this->curl = new Http\CurlDownloader($io, $config, $options, $disableTls);
  65. }
  66. $this->rfs = new RemoteFilesystem($io, $config, $options, $disableTls);
  67. }
  68. public function get($url, $options = array())
  69. {
  70. list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => false), true);
  71. $this->wait($job['id']);
  72. return $this->getResponse($job['id']);
  73. }
  74. public function add($url, $options = array())
  75. {
  76. list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => false));
  77. return $promise;
  78. }
  79. public function copy($url, $to, $options = array())
  80. {
  81. list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => $to), true);
  82. $this->wait($job['id']);
  83. return $this->getResponse($job['id']);
  84. }
  85. public function addCopy($url, $to, $options = array())
  86. {
  87. list($job, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => $to));
  88. return $promise;
  89. }
  90. /**
  91. * Retrieve the options set in the constructor
  92. *
  93. * @return array Options
  94. */
  95. public function getOptions()
  96. {
  97. return $this->options;
  98. }
  99. /**
  100. * Merges new options
  101. *
  102. * @return void
  103. */
  104. public function setOptions(array $options)
  105. {
  106. $this->options = array_replace_recursive($this->options, $options);
  107. }
  108. private function addJob($request, $sync = false)
  109. {
  110. $job = array(
  111. 'id' => $this->idGen++,
  112. 'status' => self::STATUS_QUEUED,
  113. 'request' => $request,
  114. 'sync' => $sync,
  115. 'origin' => Url::getOrigin($this->config, $request['url']),
  116. );
  117. // capture username/password from URL if there is one
  118. if (preg_match('{^https?://([^:/]+):([^@/]+)@([^/]+)}i', $request['url'], $match)) {
  119. $this->io->setAuthentication($job['origin'], rawurldecode($match[1]), rawurldecode($match[2]));
  120. }
  121. $rfs = $this->rfs;
  122. if ($this->curl && preg_match('{^https?://}i', $job['request']['url'])) {
  123. $resolver = function ($resolve, $reject) use (&$job) {
  124. $job['status'] = HttpDownloader::STATUS_QUEUED;
  125. $job['resolve'] = $resolve;
  126. $job['reject'] = $reject;
  127. };
  128. } else {
  129. $resolver = function ($resolve, $reject) use (&$job, $rfs) {
  130. // start job
  131. $url = $job['request']['url'];
  132. $options = $job['request']['options'];
  133. $job['status'] = HttpDownloader::STATUS_STARTED;
  134. if ($job['request']['copyTo']) {
  135. $result = $rfs->copy($job['origin'], $url, $job['request']['copyTo'], false /* TODO progress */, $options);
  136. $headers = $rfs->getLastHeaders();
  137. $response = new Http\Response($job['request'], $rfs->findStatusCode($headers), $headers, $job['request']['copyTo'].'~');
  138. $resolve($response);
  139. } else {
  140. $body = $rfs->getContents($job['origin'], $url, false /* TODO progress */, $options);
  141. $headers = $rfs->getLastHeaders();
  142. $response = new Http\Response($job['request'], $rfs->findStatusCode($headers), $headers, $body);
  143. $resolve($response);
  144. }
  145. };
  146. }
  147. $downloader = $this;
  148. $io = $this->io;
  149. $canceler = function () {};
  150. $promise = new Promise($resolver, $canceler);
  151. $promise->then(function ($response) use (&$job, $downloader) {
  152. $job['status'] = HttpDownloader::STATUS_COMPLETED;
  153. $job['response'] = $response;
  154. // TODO 3.0 this should be done directly on $this when PHP 5.3 is dropped
  155. $downloader->markJobDone();
  156. $downloader->scheduleNextJob();
  157. return $response;
  158. }, function ($e) use ($io, &$job, $downloader) {
  159. $job['status'] = HttpDownloader::STATUS_FAILED;
  160. $job['exception'] = $e;
  161. $downloader->markJobDone();
  162. $downloader->scheduleNextJob();
  163. throw $e;
  164. });
  165. $this->jobs[$job['id']] =& $job;
  166. if ($this->runningJobs < $this->maxJobs) {
  167. $this->startJob($job['id']);
  168. }
  169. return array($job, $promise);
  170. }
  171. private function startJob($id)
  172. {
  173. $job =& $this->jobs[$id];
  174. if ($job['status'] !== self::STATUS_QUEUED) {
  175. return;
  176. }
  177. // start job
  178. $job['status'] = self::STATUS_STARTED;
  179. $this->runningJobs++;
  180. $resolve = $job['resolve'];
  181. $reject = $job['reject'];
  182. $url = $job['request']['url'];
  183. $options = $job['request']['options'];
  184. $origin = $job['origin'];
  185. if ($this->disabled) {
  186. if (isset($job['request']['options']['http']['header']) && false !== stripos(implode('', $job['request']['options']['http']['header']), 'if-modified-since')) {
  187. $resolve(new Response(array('url' => $url), 304, array(), ''));
  188. } else {
  189. $resolve(new Response(array('url' => $url), 404, array(), ''));
  190. }
  191. return;
  192. }
  193. if ($job['request']['copyTo']) {
  194. $this->curl->download($resolve, $reject, $origin, $url, $options, $job['request']['copyTo']);
  195. } else {
  196. $this->curl->download($resolve, $reject, $origin, $url, $options);
  197. }
  198. }
  199. /**
  200. * @private
  201. */
  202. public function markJobDone()
  203. {
  204. $this->runningJobs--;
  205. }
  206. /**
  207. * @private
  208. */
  209. public function scheduleNextJob()
  210. {
  211. foreach ($this->jobs as $job) {
  212. if ($job['status'] === self::STATUS_QUEUED) {
  213. $this->startJob($job['id']);
  214. if ($this->runningJobs >= $this->maxJobs) {
  215. return;
  216. }
  217. }
  218. }
  219. }
  220. public function wait($index = null, $progress = false)
  221. {
  222. while (true) {
  223. if ($this->curl) {
  224. $this->curl->tick();
  225. }
  226. if (null !== $index) {
  227. if ($this->jobs[$index]['status'] === self::STATUS_COMPLETED || $this->jobs[$index]['status'] === self::STATUS_FAILED) {
  228. return;
  229. }
  230. } else {
  231. $done = true;
  232. foreach ($this->jobs as $job) {
  233. if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED), true)) {
  234. $done = false;
  235. break;
  236. } elseif (!$job['sync']) {
  237. unset($this->jobs[$job['id']]);
  238. }
  239. }
  240. if ($done) {
  241. return;
  242. }
  243. }
  244. usleep(1000);
  245. }
  246. }
  247. private function getResponse($index)
  248. {
  249. if (!isset($this->jobs[$index])) {
  250. throw new \LogicException('Invalid request id');
  251. }
  252. if ($this->jobs[$index]['status'] === self::STATUS_FAILED) {
  253. throw $this->jobs[$index]['exception'];
  254. }
  255. if (!isset($this->jobs[$index]['response'])) {
  256. throw new \LogicException('Response not available yet, call wait() first');
  257. }
  258. $resp = $this->jobs[$index]['response'];
  259. unset($this->jobs[$index]);
  260. return $resp;
  261. }
  262. public static function outputWarnings(IOInterface $io, $url, $data)
  263. {
  264. foreach (array('warning', 'info') as $type) {
  265. if (empty($data[$type])) {
  266. continue;
  267. }
  268. if (!empty($data[$type . '-versions'])) {
  269. $versionParser = new VersionParser();
  270. $constraint = $versionParser->parseConstraints($data[$type . '-versions']);
  271. $composer = new Constraint('==', $versionParser->normalize(Composer::getVersion()));
  272. if (!$constraint->matches($composer)) {
  273. continue;
  274. }
  275. }
  276. $io->writeError('<'.$type.'>'.ucfirst($type).' from '.$url.': '.$data[$type].'</'.$type.'>');
  277. }
  278. }
  279. }