ComposerRepository.php 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184
  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\Repository;
  12. use Composer\Package\Loader\ArrayLoader;
  13. use Composer\Package\PackageInterface;
  14. use Composer\Package\AliasPackage;
  15. use Composer\Package\Version\VersionParser;
  16. use Composer\Package\Version\StabilityFilter;
  17. use Composer\Json\JsonFile;
  18. use Composer\Cache;
  19. use Composer\Config;
  20. use Composer\Composer;
  21. use Composer\Factory;
  22. use Composer\IO\IOInterface;
  23. use Composer\Util\HttpDownloader;
  24. use Composer\Util\Loop;
  25. use Composer\Plugin\PluginEvents;
  26. use Composer\Plugin\PreFileDownloadEvent;
  27. use Composer\EventDispatcher\EventDispatcher;
  28. use Composer\Downloader\TransportException;
  29. use Composer\Semver\Constraint\ConstraintInterface;
  30. use Composer\Semver\Constraint\Constraint;
  31. use Composer\Semver\Constraint\EmptyConstraint;
  32. use Composer\Util\Http\Response;
  33. use Composer\Util\MetadataMinifier;
  34. /**
  35. * @author Jordi Boggiano <j.boggiano@seld.be>
  36. */
  37. class ComposerRepository extends ArrayRepository implements ConfigurableRepositoryInterface
  38. {
  39. private $config;
  40. private $repoConfig;
  41. private $options;
  42. private $url;
  43. private $baseUrl;
  44. private $io;
  45. private $httpDownloader;
  46. private $loop;
  47. protected $cache;
  48. protected $notifyUrl;
  49. protected $searchUrl;
  50. protected $hasProviders = false;
  51. protected $providersUrl;
  52. protected $availablePackages;
  53. protected $lazyProvidersUrl;
  54. protected $providerListing;
  55. protected $loader;
  56. private $allowSslDowngrade = false;
  57. private $eventDispatcher;
  58. private $sourceMirrors;
  59. private $distMirrors;
  60. private $degradedMode = false;
  61. private $rootData;
  62. private $hasPartialPackages;
  63. private $partialPackagesByName;
  64. /**
  65. * @var array list of package names which returned a 404 and should not be re-fetched in case loadPackage is called several times
  66. * useful for v2 metadata repositories with lazy providers
  67. */
  68. private $packagesNotFoundCache = array();
  69. /**
  70. * TODO v3 should make this private once we can drop PHP 5.3 support
  71. * @private
  72. */
  73. public $versionParser;
  74. public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null)
  75. {
  76. parent::__construct();
  77. if (!preg_match('{^[\w.]+\??://}', $repoConfig['url'])) {
  78. // assume http as the default protocol
  79. $repoConfig['url'] = 'http://'.$repoConfig['url'];
  80. }
  81. $repoConfig['url'] = rtrim($repoConfig['url'], '/');
  82. if ('https?' === substr($repoConfig['url'], 0, 6)) {
  83. $repoConfig['url'] = (extension_loaded('openssl') ? 'https' : 'http') . substr($repoConfig['url'], 6);
  84. }
  85. $urlBits = parse_url($repoConfig['url']);
  86. if ($urlBits === false || empty($urlBits['scheme'])) {
  87. throw new \UnexpectedValueException('Invalid url given for Composer repository: '.$repoConfig['url']);
  88. }
  89. if (!isset($repoConfig['options'])) {
  90. $repoConfig['options'] = array();
  91. }
  92. if (isset($repoConfig['allow_ssl_downgrade']) && true === $repoConfig['allow_ssl_downgrade']) {
  93. $this->allowSslDowngrade = true;
  94. }
  95. $this->config = $config;
  96. $this->options = $repoConfig['options'];
  97. $this->url = $repoConfig['url'];
  98. // force url for packagist.org to repo.packagist.org
  99. if (preg_match('{^(?P<proto>https?)://packagist\.org/?$}i', $this->url, $match)) {
  100. $this->url = $match['proto'].'://repo.packagist.org';
  101. }
  102. $this->baseUrl = rtrim(preg_replace('{(?:/[^/\\\\]+\.json)?(?:[?#].*)?$}', '', $this->url), '/');
  103. $this->io = $io;
  104. $this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$~');
  105. $this->versionParser = new VersionParser();
  106. $this->loader = new ArrayLoader($this->versionParser);
  107. $this->httpDownloader = $httpDownloader;
  108. $this->eventDispatcher = $eventDispatcher;
  109. $this->repoConfig = $repoConfig;
  110. $this->loop = new Loop($this->httpDownloader);
  111. }
  112. public function getRepoConfig()
  113. {
  114. return $this->repoConfig;
  115. }
  116. /**
  117. * {@inheritDoc}
  118. */
  119. public function findPackage($name, $constraint)
  120. {
  121. // this call initializes loadRootServerFile which is needed for the rest below to work
  122. $hasProviders = $this->hasProviders();
  123. $name = strtolower($name);
  124. if (!$constraint instanceof ConstraintInterface) {
  125. $constraint = $this->versionParser->parseConstraints($constraint);
  126. }
  127. if ($this->lazyProvidersUrl) {
  128. if ($this->hasPartialPackages() && isset($this->partialPackagesByName[$name])) {
  129. return $this->filterPackages($this->whatProvides($name), $constraint, true);
  130. }
  131. if (is_array($this->availablePackages) && !isset($this->availablePackages[$name])) {
  132. return;
  133. }
  134. $packages = $this->loadAsyncPackages(array($name => $constraint));
  135. return reset($packages['packages']);
  136. }
  137. if ($hasProviders) {
  138. foreach ($this->getProviderNames() as $providerName) {
  139. if ($name === $providerName) {
  140. return $this->filterPackages($this->whatProvides($providerName), $constraint, true);
  141. }
  142. }
  143. return;
  144. }
  145. return parent::findPackage($name, $constraint);
  146. }
  147. /**
  148. * {@inheritDoc}
  149. */
  150. public function findPackages($name, $constraint = null)
  151. {
  152. // this call initializes loadRootServerFile which is needed for the rest below to work
  153. $hasProviders = $this->hasProviders();
  154. $name = strtolower($name);
  155. if (null !== $constraint && !$constraint instanceof ConstraintInterface) {
  156. $constraint = $this->versionParser->parseConstraints($constraint);
  157. }
  158. if ($this->lazyProvidersUrl) {
  159. if ($this->hasPartialPackages() && isset($this->partialPackagesByName[$name])) {
  160. return $this->filterPackages($this->whatProvides($name), $constraint);
  161. }
  162. if (is_array($this->availablePackages) && !isset($this->availablePackages[$name])) {
  163. return array();
  164. }
  165. $result = $this->loadAsyncPackages(array($name => $constraint));
  166. return $result['packages'];
  167. }
  168. if ($hasProviders) {
  169. foreach ($this->getProviderNames() as $providerName) {
  170. if ($name === $providerName) {
  171. return $this->filterPackages($this->whatProvides($providerName), $constraint);
  172. }
  173. }
  174. return array();
  175. }
  176. return parent::findPackages($name, $constraint);
  177. }
  178. private function filterPackages(array $packages, $constraint = null, $returnFirstMatch = false)
  179. {
  180. if (null === $constraint) {
  181. if ($returnFirstMatch) {
  182. return reset($packages);
  183. }
  184. return $packages;
  185. }
  186. $filteredPackages = array();
  187. foreach ($packages as $package) {
  188. $pkgConstraint = new Constraint('==', $package->getVersion());
  189. if ($constraint->matches($pkgConstraint)) {
  190. if ($returnFirstMatch) {
  191. return $package;
  192. }
  193. $filteredPackages[] = $package;
  194. }
  195. }
  196. if ($returnFirstMatch) {
  197. return null;
  198. }
  199. return $filteredPackages;
  200. }
  201. public function getPackages()
  202. {
  203. $hasProviders = $this->hasProviders();
  204. if ($this->lazyProvidersUrl) {
  205. if (is_array($this->availablePackages)) {
  206. $packageMap = array();
  207. foreach ($this->availablePackages as $name) {
  208. $packageMap[$name] = new EmptyConstraint();
  209. }
  210. $result = $this->loadAsyncPackages($packageMap);
  211. return array_values($result['packages']);
  212. }
  213. if ($this->hasPartialPackages()) {
  214. return array_values($this->partialPackagesByName);
  215. }
  216. throw new \LogicException('Composer repositories that have lazy providers and no available-packages list can not load the complete list of packages, use getPackageNames instead.');
  217. }
  218. if ($hasProviders) {
  219. throw new \LogicException('Composer repositories that have providers can not load the complete list of packages, use getPackageNames instead.');
  220. }
  221. return parent::getPackages();
  222. }
  223. public function getPackageNames()
  224. {
  225. // TODO add getPackageNames to the RepositoryInterface perhaps? With filtering capability embedded?
  226. $hasProviders = $this->hasProviders();
  227. if ($this->lazyProvidersUrl) {
  228. if (is_array($this->availablePackages)) {
  229. return array_keys($this->availablePackages);
  230. }
  231. // TODO implement new list API endpoint for those repos somehow?
  232. if ($this->hasPartialPackages()) {
  233. return array_keys($this->partialPackagesByName);
  234. }
  235. return array();
  236. }
  237. if ($hasProviders) {
  238. return $this->getProviderNames();
  239. }
  240. $names = array();
  241. foreach ($this->getPackages() as $package) {
  242. $names[] = $package->getPrettyName();
  243. }
  244. return $names;
  245. }
  246. public function loadPackages(array $packageNameMap, array $acceptableStabilities, array $stabilityFlags)
  247. {
  248. // this call initializes loadRootServerFile which is needed for the rest below to work
  249. $hasProviders = $this->hasProviders();
  250. if (!$hasProviders && !$this->hasPartialPackages() && !$this->lazyProvidersUrl) {
  251. return parent::loadPackages($packageNameMap, $acceptableStabilities, $stabilityFlags);
  252. }
  253. $packages = array();
  254. $namesFound = array();
  255. if ($hasProviders || $this->hasPartialPackages()) {
  256. foreach ($packageNameMap as $name => $constraint) {
  257. $matches = array();
  258. // if a repo has no providers but only partial packages and the partial packages are missing
  259. // then we don't want to call whatProvides as it would try to load from the providers and fail
  260. if (!$hasProviders && !isset($this->partialPackagesByName[$name])) {
  261. continue;
  262. }
  263. $candidates = $this->whatProvides($name, $acceptableStabilities, $stabilityFlags);
  264. foreach ($candidates as $candidate) {
  265. if ($candidate->getName() !== $name) {
  266. throw new \LogicException('whatProvides should never return a package with a different name than the requested one');
  267. }
  268. $namesFound[$name] = true;
  269. if (!$constraint || $constraint->matches(new Constraint('==', $candidate->getVersion()))) {
  270. $matches[spl_object_hash($candidate)] = $candidate;
  271. if ($candidate instanceof AliasPackage && !isset($matches[spl_object_hash($candidate->getAliasOf())])) {
  272. $matches[spl_object_hash($candidate->getAliasOf())] = $candidate->getAliasOf();
  273. }
  274. }
  275. }
  276. // add aliases of matched packages even if they did not match the constraint
  277. foreach ($candidates as $candidate) {
  278. if ($candidate instanceof AliasPackage) {
  279. if (isset($matches[spl_object_hash($candidate->getAliasOf())])) {
  280. $matches[spl_object_hash($candidate)] = $candidate;
  281. }
  282. }
  283. }
  284. $packages = array_merge($packages, $matches);
  285. unset($packageNameMap[$name]);
  286. }
  287. }
  288. if ($this->lazyProvidersUrl && count($packageNameMap)) {
  289. if (is_array($this->availablePackages)) {
  290. $availPackages = $this->availablePackages;
  291. $packageNameMap = array_filter($packageNameMap, function ($name) use ($availPackages) {
  292. return isset($availPackages[strtolower($name)]);
  293. }, ARRAY_FILTER_USE_KEY);
  294. }
  295. $result = $this->loadAsyncPackages($packageNameMap, $acceptableStabilities, $stabilityFlags);
  296. $packages = array_merge($packages, $result['packages']);
  297. $namesFound = array_merge($namesFound, $result['namesFound']);
  298. }
  299. return array('namesFound' => array_keys($namesFound), 'packages' => $packages);
  300. }
  301. /**
  302. * {@inheritDoc}
  303. */
  304. public function search($query, $mode = 0, $type = null)
  305. {
  306. $this->loadRootServerFile();
  307. if ($this->searchUrl && $mode === self::SEARCH_FULLTEXT) {
  308. $url = str_replace(array('%query%', '%type%'), array($query, $type), $this->searchUrl);
  309. $search = $this->httpDownloader->get($url, $this->options)->decodeJson();
  310. if (empty($search['results'])) {
  311. return array();
  312. }
  313. $results = array();
  314. foreach ($search['results'] as $result) {
  315. // do not show virtual packages in results as they are not directly useful from a composer perspective
  316. if (empty($result['virtual'])) {
  317. $results[] = $result;
  318. }
  319. }
  320. return $results;
  321. }
  322. if ($this->hasProviders() || $this->lazyProvidersUrl) {
  323. $results = array();
  324. $regex = '{(?:'.implode('|', preg_split('{\s+}', $query)).')}i';
  325. foreach ($this->getPackageNames() as $name) {
  326. if (preg_match($regex, $name)) {
  327. $results[] = array('name' => $name);
  328. }
  329. }
  330. return $results;
  331. }
  332. return parent::search($query, $mode);
  333. }
  334. private function getProviderNames()
  335. {
  336. $this->loadRootServerFile();
  337. if (null === $this->providerListing) {
  338. $this->loadProviderListings($this->loadRootServerFile());
  339. }
  340. if ($this->lazyProvidersUrl) {
  341. // Can not determine list of provided packages for lazy repositories
  342. return array();
  343. }
  344. if ($this->providersUrl) {
  345. return array_keys($this->providerListing);
  346. }
  347. return array();
  348. }
  349. private function configurePackageTransportOptions(PackageInterface $package)
  350. {
  351. foreach ($package->getDistUrls() as $url) {
  352. if (strpos($url, $this->baseUrl) === 0) {
  353. $package->setTransportOptions($this->options);
  354. return;
  355. }
  356. }
  357. }
  358. private function hasProviders()
  359. {
  360. $this->loadRootServerFile();
  361. return $this->hasProviders;
  362. }
  363. /**
  364. * @param string $name package name
  365. * @param callable $isPackageAcceptableCallable
  366. * @return array|mixed
  367. */
  368. private function whatProvides($name, array $acceptableStabilities = null, array $stabilityFlags = null)
  369. {
  370. if (!$this->hasPartialPackages() || !isset($this->partialPackagesByName[$name])) {
  371. // skip platform packages, root package and composer-plugin-api
  372. if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name) || '__root__' === $name || 'composer-plugin-api' === $name) {
  373. return array();
  374. }
  375. if (null === $this->providerListing) {
  376. $this->loadProviderListings($this->loadRootServerFile());
  377. }
  378. $useLastModifiedCheck = false;
  379. if ($this->lazyProvidersUrl && !isset($this->providerListing[$name])) {
  380. $hash = null;
  381. $url = str_replace('%package%', $name, $this->lazyProvidersUrl);
  382. $cacheKey = 'provider-'.strtr($name, '/', '$').'.json';
  383. $useLastModifiedCheck = true;
  384. } elseif ($this->providersUrl) {
  385. // package does not exist in this repo
  386. if (!isset($this->providerListing[$name])) {
  387. return array();
  388. }
  389. $hash = $this->providerListing[$name]['sha256'];
  390. $url = str_replace(array('%package%', '%hash%'), array($name, $hash), $this->providersUrl);
  391. $cacheKey = 'provider-'.strtr($name, '/', '$').'.json';
  392. } else {
  393. return array();
  394. }
  395. $packages = null;
  396. if ($cacheKey) {
  397. if (!$useLastModifiedCheck && $hash && $this->cache->sha256($cacheKey) === $hash) {
  398. $packages = json_decode($this->cache->read($cacheKey), true);
  399. } elseif ($useLastModifiedCheck) {
  400. if ($contents = $this->cache->read($cacheKey)) {
  401. $contents = json_decode($contents, true);
  402. if (isset($contents['last-modified'])) {
  403. $response = $this->fetchFileIfLastModified($url, $cacheKey, $contents['last-modified']);
  404. if (true === $response) {
  405. $packages = $contents;
  406. } elseif ($response) {
  407. $packages = $response;
  408. }
  409. }
  410. }
  411. }
  412. }
  413. if (!$packages) {
  414. try {
  415. $packages = $this->fetchFile($url, $cacheKey, $hash, $useLastModifiedCheck);
  416. } catch (TransportException $e) {
  417. // 404s are acceptable for lazy provider repos
  418. if ($e->getStatusCode() === 404 && $this->lazyProvidersUrl) {
  419. $packages = array('packages' => array());
  420. } else {
  421. throw $e;
  422. }
  423. }
  424. }
  425. $loadingPartialPackage = false;
  426. } else {
  427. $packages = array('packages' => array('versions' => $this->partialPackagesByName[$name]));
  428. $loadingPartialPackage = true;
  429. }
  430. $result = array();
  431. $versionsToLoad = array();
  432. foreach ($packages['packages'] as $versions) {
  433. foreach ($versions as $version) {
  434. $normalizedName = strtolower($version['name']);
  435. // only load the actual named package, not other packages that might find themselves in the same file
  436. if ($normalizedName !== $name) {
  437. continue;
  438. }
  439. if (!$loadingPartialPackage && $this->hasPartialPackages() && isset($this->partialPackagesByName[$normalizedName])) {
  440. continue;
  441. }
  442. if (!isset($versionsToLoad[$version['uid']])) {
  443. if (!isset($version['version_normalized'])) {
  444. $version['version_normalized'] = $this->versionParser->normalize($version['version']);
  445. }
  446. if ($this->isVersionAcceptable($acceptableStabilities, $stabilityFlags, null, $normalizedName, $version)) {
  447. $versionsToLoad[$version['uid']] = $version;
  448. }
  449. }
  450. }
  451. }
  452. // load acceptable packages in the providers
  453. $loadedPackages = $this->createPackages($versionsToLoad, 'Composer\Package\CompletePackage');
  454. $uids = array_keys($versionsToLoad);
  455. foreach ($loadedPackages as $index => $package) {
  456. $package->setRepository($this);
  457. $uid = $uids[$index];
  458. if ($package instanceof AliasPackage) {
  459. $aliased = $package->getAliasOf();
  460. $aliased->setRepository($this);
  461. $result[$uid] = $aliased;
  462. $result[$uid.'-alias'] = $package;
  463. } else {
  464. $result[$uid] = $package;
  465. }
  466. }
  467. return $result;
  468. }
  469. /**
  470. * {@inheritDoc}
  471. */
  472. protected function initialize()
  473. {
  474. parent::initialize();
  475. $repoData = $this->loadDataFromServer();
  476. foreach ($this->createPackages($repoData, 'Composer\Package\CompletePackage') as $package) {
  477. $this->addPackage($package);
  478. }
  479. }
  480. /**
  481. * Adds a new package to the repository
  482. *
  483. * @param PackageInterface $package
  484. */
  485. public function addPackage(PackageInterface $package)
  486. {
  487. parent::addPackage($package);
  488. $this->configurePackageTransportOptions($package);
  489. }
  490. /**
  491. * @param array $packageNames array of package name => ConstraintInterface|null - if a constraint is provided, only packages matching it will be loaded
  492. */
  493. private function loadAsyncPackages(array $packageNames, array $acceptableStabilities = null, array $stabilityFlags = null)
  494. {
  495. $this->loadRootServerFile();
  496. $packages = array();
  497. $namesFound = array();
  498. $promises = array();
  499. $repo = $this;
  500. if (!$this->lazyProvidersUrl) {
  501. throw new \LogicException('loadAsyncPackages only supports v2 protocol composer repos with a metadata-url');
  502. }
  503. // load ~dev versions of the packages as well if needed
  504. foreach ($packageNames as $name => $constraint) {
  505. if ($acceptableStabilities && $stabilityFlags && StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, array($name), 'dev')) {
  506. $packageNames[$name.'~dev'] = $constraint;
  507. }
  508. }
  509. foreach ($packageNames as $name => $constraint) {
  510. $name = strtolower($name);
  511. $realName = preg_replace('{~dev$}', '', $name);
  512. // skip platform packages, root package and composer-plugin-api
  513. if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $realName) || '__root__' === $realName || 'composer-plugin-api' === $realName) {
  514. continue;
  515. }
  516. $url = str_replace('%package%', $name, $this->lazyProvidersUrl);
  517. $cacheKey = 'provider-'.strtr($name, '/', '~').'.json';
  518. $lastModified = null;
  519. if ($contents = $this->cache->read($cacheKey)) {
  520. $contents = json_decode($contents, true);
  521. $lastModified = isset($contents['last-modified']) ? $contents['last-modified'] : null;
  522. }
  523. $promises[] = $this->asyncFetchFile($url, $cacheKey, $lastModified)
  524. ->then(function ($response) use (&$packages, &$namesFound, $contents, $realName, $constraint, $repo, $acceptableStabilities, $stabilityFlags) {
  525. if (true === $response) {
  526. $response = $contents;
  527. }
  528. if (!isset($response['packages'][$realName])) {
  529. return;
  530. }
  531. $versions = $response['packages'][$realName];
  532. if (isset($response['minified']) && $response['minified'] === 'composer/2.0') {
  533. $versions = MetadataMinifier::expand($versions);
  534. }
  535. $namesFound[$realName] = true;
  536. $versionsToLoad = array();
  537. foreach ($versions as $version) {
  538. if (!isset($version['version_normalized'])) {
  539. $version['version_normalized'] = $repo->versionParser->normalize($version['version']);
  540. }
  541. if ($repo->isVersionAcceptable($acceptableStabilities, $stabilityFlags, $constraint, $realName, $version)) {
  542. $versionsToLoad[] = $version;
  543. }
  544. }
  545. $loadedPackages = $repo->createPackages($versionsToLoad, 'Composer\Package\CompletePackage');
  546. foreach ($loadedPackages as $package) {
  547. $package->setRepository($repo);
  548. $packages[spl_object_hash($package)] = $package;
  549. if ($package instanceof AliasPackage && !isset($packages[spl_object_hash($package->getAliasOf())])) {
  550. $package->getAliasOf()->setRepository($repo);
  551. $packages[spl_object_hash($package->getAliasOf())] = $package->getAliasOf();
  552. }
  553. }
  554. });
  555. }
  556. $this->loop->wait($promises);
  557. return array('namesFound' => $namesFound, 'packages' => $packages);
  558. // RepositorySet should call loadMetadata, getMetadata when all promises resolved, then metadataComplete when done so we can GC the loaded json and whatnot then as needed
  559. }
  560. /**
  561. * TODO v3 should make this private once we can drop PHP 5.3 support
  562. *
  563. * @param string $name package name (must be lowercased already)
  564. * @private
  565. */
  566. public function isVersionAcceptable(array $acceptableStabilities = null, array $stabilityFlags = null, $constraint = null, $name, $versionData)
  567. {
  568. $versions = array($versionData['version_normalized']);
  569. if ($alias = $this->loader->getBranchAlias($versionData)) {
  570. $versions[] = $alias;
  571. }
  572. foreach ($versions as $version) {
  573. if ($acceptableStabilities && $stabilityFlags && !StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, array($name), VersionParser::parseStability($version))) {
  574. continue;
  575. }
  576. if ($constraint && !$constraint->matches(new Constraint('==', $version))) {
  577. continue;
  578. }
  579. return true;
  580. }
  581. return false;
  582. }
  583. protected function loadRootServerFile()
  584. {
  585. if (null !== $this->rootData) {
  586. return $this->rootData;
  587. }
  588. if (!extension_loaded('openssl') && 'https' === substr($this->url, 0, 5)) {
  589. throw new \RuntimeException('You must enable the openssl extension in your php.ini to load information from '.$this->url);
  590. }
  591. $jsonUrlParts = parse_url($this->url);
  592. if (isset($jsonUrlParts['path']) && false !== strpos($jsonUrlParts['path'], '.json')) {
  593. $jsonUrl = $this->url;
  594. } else {
  595. $jsonUrl = $this->url . '/packages.json';
  596. }
  597. $data = $this->fetchFile($jsonUrl, 'packages.json');
  598. if (!empty($data['notify-batch'])) {
  599. $this->notifyUrl = $this->canonicalizeUrl($data['notify-batch']);
  600. } elseif (!empty($data['notify'])) {
  601. $this->notifyUrl = $this->canonicalizeUrl($data['notify']);
  602. }
  603. if (!empty($data['search'])) {
  604. $this->searchUrl = $this->canonicalizeUrl($data['search']);
  605. }
  606. if (!empty($data['mirrors'])) {
  607. foreach ($data['mirrors'] as $mirror) {
  608. if (!empty($mirror['git-url'])) {
  609. $this->sourceMirrors['git'][] = array('url' => $mirror['git-url'], 'preferred' => !empty($mirror['preferred']));
  610. }
  611. if (!empty($mirror['hg-url'])) {
  612. $this->sourceMirrors['hg'][] = array('url' => $mirror['hg-url'], 'preferred' => !empty($mirror['preferred']));
  613. }
  614. if (!empty($mirror['dist-url'])) {
  615. $this->distMirrors[] = array(
  616. 'url' => $this->canonicalizeUrl($mirror['dist-url']),
  617. 'preferred' => !empty($mirror['preferred']),
  618. );
  619. }
  620. }
  621. }
  622. if (!empty($data['providers-lazy-url'])) {
  623. $this->lazyProvidersUrl = $this->canonicalizeUrl($data['providers-lazy-url']);
  624. $this->hasProviders = true;
  625. $this->hasPartialPackages = !empty($data['packages']) && is_array($data['packages']);
  626. }
  627. // metadata-url indiates V2 repo protocol so it takes over from all the V1 types
  628. // V2 only has lazyProviders and possibly partial packages, but no ability to process anything else,
  629. // V2 also supports async loading
  630. if (!empty($data['metadata-url'])) {
  631. $this->lazyProvidersUrl = $this->canonicalizeUrl($data['metadata-url']);
  632. $this->providersUrl = null;
  633. $this->hasProviders = false;
  634. $this->hasPartialPackages = !empty($data['packages']) && is_array($data['packages']);
  635. $this->allowSslDowngrade = false;
  636. // provides a list of package names that are available in this repo
  637. // this disables lazy-provider behavior in the sense that if a list is available we assume it is finite and won't search for other packages in that repo
  638. // while if no list is there lazyProvidersUrl is used when looking for any package name to see if the repo knows it
  639. if (!empty($data['available-packages'])) {
  640. $availPackages = array_map('strtolower', $data['available-packages']);
  641. $this->availablePackages = array_combine($availPackages, $availPackages);
  642. }
  643. // Remove legacy keys as most repos need to be compatible with Composer v1
  644. // as well but we are not interested in the old format anymore at this point
  645. unset($data['providers-url'], $data['providers'], $data['providers-includes']);
  646. }
  647. if ($this->allowSslDowngrade) {
  648. $this->url = str_replace('https://', 'http://', $this->url);
  649. $this->baseUrl = str_replace('https://', 'http://', $this->baseUrl);
  650. }
  651. if (!empty($data['providers-url'])) {
  652. $this->providersUrl = $this->canonicalizeUrl($data['providers-url']);
  653. $this->hasProviders = true;
  654. }
  655. if (!empty($data['providers']) || !empty($data['providers-includes'])) {
  656. $this->hasProviders = true;
  657. }
  658. return $this->rootData = $data;
  659. }
  660. private function canonicalizeUrl($url)
  661. {
  662. if ('/' === $url[0]) {
  663. if (preg_match('{^[^:]++://[^/]*+}', $this->url, $matches)) {
  664. return $matches[0] . $url;
  665. }
  666. return $this->url;
  667. }
  668. return $url;
  669. }
  670. private function loadDataFromServer()
  671. {
  672. $data = $this->loadRootServerFile();
  673. return $this->loadIncludes($data);
  674. }
  675. private function hasPartialPackages()
  676. {
  677. if ($this->hasPartialPackages && null === $this->partialPackagesByName) {
  678. $this->initializePartialPackages();
  679. }
  680. return $this->hasPartialPackages;
  681. }
  682. private function loadProviderListings($data)
  683. {
  684. if (isset($data['providers'])) {
  685. if (!is_array($this->providerListing)) {
  686. $this->providerListing = array();
  687. }
  688. $this->providerListing = array_merge($this->providerListing, $data['providers']);
  689. }
  690. if ($this->providersUrl && isset($data['provider-includes'])) {
  691. $includes = $data['provider-includes'];
  692. foreach ($includes as $include => $metadata) {
  693. $url = $this->baseUrl . '/' . str_replace('%hash%', $metadata['sha256'], $include);
  694. $cacheKey = str_replace(array('%hash%','$'), '', $include);
  695. if ($this->cache->sha256($cacheKey) === $metadata['sha256']) {
  696. $includedData = json_decode($this->cache->read($cacheKey), true);
  697. } else {
  698. $includedData = $this->fetchFile($url, $cacheKey, $metadata['sha256']);
  699. }
  700. $this->loadProviderListings($includedData);
  701. }
  702. }
  703. }
  704. private function loadIncludes($data)
  705. {
  706. $packages = array();
  707. // legacy repo handling
  708. if (!isset($data['packages']) && !isset($data['includes'])) {
  709. foreach ($data as $pkg) {
  710. foreach ($pkg['versions'] as $metadata) {
  711. $packages[] = $metadata;
  712. }
  713. }
  714. return $packages;
  715. }
  716. if (isset($data['packages'])) {
  717. foreach ($data['packages'] as $package => $versions) {
  718. foreach ($versions as $version => $metadata) {
  719. $packages[] = $metadata;
  720. }
  721. }
  722. }
  723. if (isset($data['includes'])) {
  724. foreach ($data['includes'] as $include => $metadata) {
  725. if ($this->cache->sha1($include) === $metadata['sha1']) {
  726. $includedData = json_decode($this->cache->read($include), true);
  727. } else {
  728. $includedData = $this->fetchFile($include);
  729. }
  730. $packages = array_merge($packages, $this->loadIncludes($includedData));
  731. }
  732. }
  733. return $packages;
  734. }
  735. /**
  736. * TODO v3 should make this private once we can drop PHP 5.3 support
  737. *
  738. * @private
  739. */
  740. public function createPackages(array $packages, $class = 'Composer\Package\CompletePackage')
  741. {
  742. if (!$packages) {
  743. return array();
  744. }
  745. try {
  746. foreach ($packages as &$data) {
  747. if (!isset($data['notification-url'])) {
  748. $data['notification-url'] = $this->notifyUrl;
  749. }
  750. }
  751. $packages = $this->loader->loadPackages($packages, $class);
  752. foreach ($packages as $package) {
  753. if (isset($this->sourceMirrors[$package->getSourceType()])) {
  754. $package->setSourceMirrors($this->sourceMirrors[$package->getSourceType()]);
  755. }
  756. $package->setDistMirrors($this->distMirrors);
  757. $this->configurePackageTransportOptions($package);
  758. }
  759. return $packages;
  760. } catch (\Exception $e) {
  761. throw new \RuntimeException('Could not load packages '.(isset($packages[0]['name']) ? $packages[0]['name'] : json_encode($packages)).' in '.$this->url.': ['.get_class($e).'] '.$e->getMessage(), 0, $e);
  762. }
  763. }
  764. protected function fetchFile($filename, $cacheKey = null, $sha256 = null, $storeLastModifiedTime = false)
  765. {
  766. if (null === $cacheKey) {
  767. $cacheKey = $filename;
  768. $filename = $this->baseUrl.'/'.$filename;
  769. }
  770. // url-encode $ signs in URLs as bad proxies choke on them
  771. if (($pos = strpos($filename, '$')) && preg_match('{^https?://.*}i', $filename)) {
  772. $filename = substr($filename, 0, $pos) . '%24' . substr($filename, $pos + 1);
  773. }
  774. $retries = 3;
  775. while ($retries--) {
  776. try {
  777. if ($this->eventDispatcher) {
  778. $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename);
  779. $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
  780. }
  781. $response = $this->httpDownloader->get($filename, $this->options);
  782. $json = $response->getBody();
  783. if ($sha256 && $sha256 !== hash('sha256', $json)) {
  784. // undo downgrade before trying again if http seems to be hijacked or modifying content somehow
  785. if ($this->allowSslDowngrade) {
  786. $this->url = str_replace('http://', 'https://', $this->url);
  787. $this->baseUrl = str_replace('http://', 'https://', $this->baseUrl);
  788. $filename = str_replace('http://', 'https://', $filename);
  789. }
  790. if ($retries) {
  791. usleep(100000);
  792. continue;
  793. }
  794. // TODO use scarier wording once we know for sure it doesn't do false positives anymore
  795. throw new RepositorySecurityException('The contents of '.$filename.' do not match its signature. This could indicate a man-in-the-middle attack or e.g. antivirus software corrupting files. Try running composer again and report this if you think it is a mistake.');
  796. }
  797. $data = $response->decodeJson();
  798. HttpDownloader::outputWarnings($this->io, $this->url, $data);
  799. if ($cacheKey) {
  800. if ($storeLastModifiedTime) {
  801. $lastModifiedDate = $response->getHeader('last-modified');
  802. if ($lastModifiedDate) {
  803. $data['last-modified'] = $lastModifiedDate;
  804. $json = json_encode($data);
  805. }
  806. }
  807. $this->cache->write($cacheKey, $json);
  808. }
  809. $response->collect();
  810. break;
  811. } catch (\Exception $e) {
  812. if ($e instanceof \LogicException) {
  813. throw $e;
  814. }
  815. if ($e instanceof TransportException && $e->getStatusCode() === 404) {
  816. throw $e;
  817. }
  818. if ($retries) {
  819. usleep(100000);
  820. continue;
  821. }
  822. if ($e instanceof RepositorySecurityException) {
  823. throw $e;
  824. }
  825. if ($cacheKey && ($contents = $this->cache->read($cacheKey))) {
  826. if (!$this->degradedMode) {
  827. $this->io->writeError('<warning>'.$e->getMessage().'</warning>');
  828. $this->io->writeError('<warning>'.$this->url.' could not be fully loaded, package information was loaded from the local cache and may be out of date</warning>');
  829. }
  830. $this->degradedMode = true;
  831. $data = JsonFile::parseJson($contents, $this->cache->getRoot().$cacheKey);
  832. break;
  833. }
  834. throw $e;
  835. }
  836. }
  837. return $data;
  838. }
  839. private function fetchFileIfLastModified($filename, $cacheKey, $lastModifiedTime)
  840. {
  841. $retries = 3;
  842. while ($retries--) {
  843. try {
  844. if ($this->eventDispatcher) {
  845. $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename);
  846. $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
  847. }
  848. $options = $this->options;
  849. if (isset($options['http']['header'])) {
  850. $options['http']['header'] = (array) $options['http']['header'];
  851. }
  852. $options['http']['header'][] = array('If-Modified-Since: '.$lastModifiedTime);
  853. $response = $this->httpDownloader->get($filename, $options);
  854. $json = $response->getBody();
  855. if ($json === '' && $response->getStatusCode() === 304) {
  856. return true;
  857. }
  858. $data = $response->decodeJson();
  859. HttpDownloader::outputWarnings($this->io, $this->url, $data);
  860. $lastModifiedDate = $response->getHeader('last-modified');
  861. $response->collect();
  862. if ($lastModifiedDate) {
  863. $data['last-modified'] = $lastModifiedDate;
  864. $json = json_encode($data);
  865. }
  866. $this->cache->write($cacheKey, $json);
  867. return $data;
  868. } catch (\Exception $e) {
  869. if ($e instanceof \LogicException) {
  870. throw $e;
  871. }
  872. if ($e instanceof TransportException && $e->getStatusCode() === 404) {
  873. throw $e;
  874. }
  875. if ($retries) {
  876. usleep(100000);
  877. continue;
  878. }
  879. if (!$this->degradedMode) {
  880. $this->io->writeError('<warning>'.$e->getMessage().'</warning>');
  881. $this->io->writeError('<warning>'.$this->url.' could not be fully loaded, package information was loaded from the local cache and may be out of date</warning>');
  882. }
  883. $this->degradedMode = true;
  884. return true;
  885. }
  886. }
  887. }
  888. private function asyncFetchFile($filename, $cacheKey, $lastModifiedTime = null)
  889. {
  890. $retries = 3;
  891. if (isset($this->packagesNotFoundCache[$filename])) {
  892. return \React\Promise\Util::promiseFor(array('packages' => array()));
  893. }
  894. $httpDownloader = $this->httpDownloader;
  895. if ($this->eventDispatcher) {
  896. $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename);
  897. $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
  898. }
  899. $options = $lastModifiedTime ? array('http' => array('header' => array('If-Modified-Since: '.$lastModifiedTime))) : array();
  900. $io = $this->io;
  901. $url = $this->url;
  902. $cache = $this->cache;
  903. $degradedMode =& $this->degradedMode;
  904. $accept = function ($response) use ($io, $url, $cache, $cacheKey) {
  905. // package not found is acceptable for a v2 protocol repository
  906. if ($response->getStatusCode() === 404) {
  907. $this->packagesNotFoundCache[$filename] = true;
  908. return array('packages' => array());
  909. }
  910. $json = $response->getBody();
  911. if ($json === '' && $response->getStatusCode() === 304) {
  912. return true;
  913. }
  914. $data = $response->decodeJson();
  915. HttpDownloader::outputWarnings($io, $url, $data);
  916. $lastModifiedDate = $response->getHeader('last-modified');
  917. $response->collect();
  918. if ($lastModifiedDate) {
  919. $data['last-modified'] = $lastModifiedDate;
  920. $json = JsonFile::encode($data, JsonFile::JSON_UNESCAPED_SLASHES | JsonFile::JSON_UNESCAPED_UNICODE);
  921. }
  922. $cache->write($cacheKey, $json);
  923. return $data;
  924. };
  925. $reject = function ($e) use (&$retries, $httpDownloader, $filename, $options, &$reject, $accept, $io, $url, &$degradedMode) {
  926. if ($e instanceof TransportException && $e->getStatusCode() === 404) {
  927. $this->packagesNotFoundCache[$filename] = true;
  928. return false;
  929. }
  930. // special error code returned when network is being artificially disabled
  931. if ($e instanceof TransportException && $e->getStatusCode() === 499) {
  932. $retries = 0;
  933. }
  934. if (--$retries > 0) {
  935. usleep(100000);
  936. return $httpDownloader->add($filename, $options)->then($accept, $reject);
  937. }
  938. if (!$degradedMode) {
  939. $io->writeError('<warning>'.$e->getMessage().'</warning>');
  940. $io->writeError('<warning>'.$url.' could not be fully loaded, package information was loaded from the local cache and may be out of date</warning>');
  941. }
  942. $degradedMode = true;
  943. // special error code returned when network is being artificially disabled
  944. if ($e instanceof TransportException && $e->getStatusCode() === 499) {
  945. return $accept(new Response(array('url' => $url), 404, array(), ''));
  946. }
  947. throw $e;
  948. };
  949. return $httpDownloader->add($filename, $options)->then($accept, $reject);
  950. }
  951. /**
  952. * This initializes the packages key of a partial packages.json that contain some packages inlined + a providers-lazy-url
  953. *
  954. * This should only be called once
  955. */
  956. private function initializePartialPackages()
  957. {
  958. $rootData = $this->loadRootServerFile();
  959. $this->partialPackagesByName = array();
  960. foreach ($rootData['packages'] as $package => $versions) {
  961. foreach ($versions as $version) {
  962. $this->partialPackagesByName[strtolower($version['name'])][] = $version;
  963. }
  964. }
  965. // wipe rootData as it is fully consumed at this point and this saves some memory
  966. $this->rootData = true;
  967. }
  968. }