PearRepository.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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\Util\StreamContextFactory;
  14. /**
  15. * @author Benjamin Eberlei <kontakt@beberlei.de>
  16. * @author Jordi Boggiano <j.boggiano@seld.be>
  17. */
  18. class PearRepository extends ArrayRepository
  19. {
  20. private $url;
  21. private $channel;
  22. private $streamContext;
  23. public function __construct(array $config)
  24. {
  25. if (!preg_match('{^https?://}', $config['url'])) {
  26. $config['url'] = 'http://'.$config['url'];
  27. }
  28. if (!filter_var($config['url'], FILTER_VALIDATE_URL)) {
  29. throw new \UnexpectedValueException('Invalid url given for PEAR repository: '.$config['url']);
  30. }
  31. $this->url = rtrim($config['url'], '/');
  32. $this->channel = !empty($config['channel']) ? $config['channel'] : null;
  33. }
  34. protected function initialize()
  35. {
  36. parent::initialize();
  37. set_error_handler(function($severity, $message, $file, $line) {
  38. throw new \ErrorException($message, $severity, $severity, $file, $line);
  39. });
  40. $this->streamContext = StreamContextFactory::getContext();
  41. $this->fetchFromServer();
  42. restore_error_handler();
  43. }
  44. protected function fetchFromServer()
  45. {
  46. if (!$this->channel) {
  47. $channelXML = $this->requestXml($this->url . "/channel.xml");
  48. $this->channel = $channelXML->getElementsByTagName("suggestedalias")->item(0)->nodeValue
  49. ?: $channelXML->getElementsByTagName("name")->item(0)->nodeValue;
  50. }
  51. $categoryXML = $this->requestXml($this->url . "/rest/c/categories.xml");
  52. $categories = $categoryXML->getElementsByTagName("c");
  53. foreach ($categories as $category) {
  54. $link = '/' . ltrim($category->getAttribute("xlink:href"), '/');
  55. try {
  56. $packagesLink = str_replace("info.xml", "packagesinfo.xml", $link);
  57. $this->fetchPear2Packages($this->url . $packagesLink);
  58. } catch (\ErrorException $e) {
  59. if (false === strpos($e->getMessage(), '404')) {
  60. throw $e;
  61. }
  62. $categoryLink = str_replace("info.xml", "packages.xml", $link);
  63. $this->fetchPearPackages($this->url . $categoryLink);
  64. }
  65. }
  66. }
  67. /**
  68. * @param string $categoryLink
  69. * @throws ErrorException
  70. * @throws InvalidArgumentException
  71. */
  72. private function fetchPearPackages($categoryLink)
  73. {
  74. $packagesXML = $this->requestXml($categoryLink);
  75. $packages = $packagesXML->getElementsByTagName('p');
  76. $loader = new ArrayLoader();
  77. foreach ($packages as $package) {
  78. $packageName = 'pear-'.$this->channel.'/'.$package->nodeValue;
  79. $packageLink = $package->getAttribute('xlink:href');
  80. $releaseLink = $this->url . str_replace("/rest/p/", "/rest/r/", $packageLink);
  81. $allReleasesLink = $releaseLink . "/allreleases2.xml";
  82. try {
  83. $releasesXML = $this->requestXml($allReleasesLink);
  84. } catch (\ErrorException $e) {
  85. if (strpos($e->getMessage(), '404')) {
  86. continue;
  87. }
  88. throw $e;
  89. }
  90. $releases = $releasesXML->getElementsByTagName('r');
  91. foreach ($releases as $release) {
  92. /* @var $release \DOMElement */
  93. $pearVersion = $release->getElementsByTagName('v')->item(0)->nodeValue;
  94. $packageData = array(
  95. 'name' => $packageName,
  96. 'type' => 'library',
  97. 'dist' => array('type' => 'pear', 'url' => $this->url.'/get/'.$packageName.'-'.$pearVersion.".tgz"),
  98. 'version' => $pearVersion,
  99. );
  100. try {
  101. $deps = file_get_contents($releaseLink . "/deps.".$pearVersion.".txt", false, $this->streamContext);
  102. } catch (\ErrorException $e) {
  103. if (strpos($e->getMessage(), '404')) {
  104. continue;
  105. }
  106. throw $e;
  107. }
  108. $packageData += $this->parseDependencies($deps);
  109. try {
  110. $this->addPackage($loader->load($packageData));
  111. } catch (\UnexpectedValueException $e) {
  112. continue;
  113. }
  114. }
  115. }
  116. }
  117. /**
  118. * @param array $data
  119. * @return string
  120. */
  121. private function parseVersion(array $data)
  122. {
  123. if (!isset($data['min']) && !isset($data['max'])) {
  124. return '*';
  125. }
  126. $versions = array();
  127. if (isset($data['min'])) {
  128. $versions[] = '>=' . $data['min'];
  129. }
  130. if (isset($data['max'])) {
  131. $versions[] = '<=' . $data['max'];
  132. }
  133. return implode(',', $versions);
  134. }
  135. /**
  136. * @todo Improve dependencies resolution of pear packages.
  137. * @param array $options
  138. * @return array
  139. */
  140. private function parseDependenciesOptions(array $depsOptions)
  141. {
  142. $data = array();
  143. foreach ($depsOptions as $name => $options) {
  144. // make sure single deps are wrapped in an array
  145. if (isset($options['name'])) {
  146. $options = array($options);
  147. }
  148. if ('php' == $name) {
  149. $data[$name] = $this->parseVersion($options);
  150. } elseif ('package' == $name) {
  151. foreach ($options as $key => $value) {
  152. if (is_array($value)) {
  153. $dataKey = $value['name'];
  154. $data[$dataKey] = $this->parseVersion($value);
  155. }
  156. }
  157. } elseif ('extension' == $name) {
  158. foreach ($options as $key => $value) {
  159. $dataKey = 'ext-' . $value['name'];
  160. $data[$dataKey] = $this->parseVersion($value);
  161. }
  162. }
  163. }
  164. return $data;
  165. }
  166. /**
  167. * @param string $deps
  168. * @return array
  169. * @throws InvalidArgumentException
  170. */
  171. private function parseDependencies($deps)
  172. {
  173. if (preg_match('((O:([0-9])+:"([^"]+)"))', $deps, $matches)) {
  174. if (strlen($matches[3]) == $matches[2]) {
  175. throw new \InvalidArgumentException("Invalid dependency data, it contains serialized objects.");
  176. }
  177. }
  178. $deps = (array) @unserialize($deps);
  179. unset($deps['required']['pearinstaller']);
  180. $depsData = array();
  181. if (!empty($deps['required'])) {
  182. $depsData['require'] = $this->parseDependenciesOptions($deps['required']);
  183. }
  184. if (!empty($deps['optional'])) {
  185. $depsData['suggest'] = $this->parseDependenciesOptions($deps['optional']);
  186. }
  187. return $depsData;
  188. }
  189. /**
  190. * @param string $packagesLink
  191. * @return void
  192. * @throws InvalidArgumentException
  193. */
  194. private function fetchPear2Packages($packagesLink)
  195. {
  196. $loader = new ArrayLoader();
  197. $packagesXml = $this->requestXml($packagesLink);
  198. $informations = $packagesXml->getElementsByTagName('pi');
  199. foreach ($informations as $information) {
  200. $package = $information->getElementsByTagName('p')->item(0);
  201. $packageName = 'pear-'.$this->channel.'/'.$package->getElementsByTagName('n')->item(0)->nodeValue;
  202. $packageData = array(
  203. 'name' => $packageName,
  204. 'type' => 'library'
  205. );
  206. $packageKeys = array('l' => 'license', 'd' => 'description');
  207. foreach ($packageKeys as $pear => $composer) {
  208. if ($package->getElementsByTagName($pear)->length > 0
  209. && ($pear = $package->getElementsByTagName($pear)->item(0)->nodeValue)) {
  210. $packageData[$composer] = $pear;
  211. }
  212. }
  213. $depsData = array();
  214. foreach ($information->getElementsByTagName('deps') as $depElement) {
  215. $depsVersion = $depElement->getElementsByTagName('v')->item(0)->nodeValue;
  216. $depsData[$depsVersion] = $this->parseDependencies(
  217. $depElement->getElementsByTagName('d')->item(0)->nodeValue
  218. );
  219. }
  220. $releases = $information->getElementsByTagName('a')->item(0);
  221. if (!$releases) {
  222. continue;
  223. }
  224. $releases = $releases->getElementsByTagName('r');
  225. $packageUrl = $this->url . '/get/' . $packageName;
  226. foreach ($releases as $release) {
  227. $version = $release->getElementsByTagName('v')->item(0)->nodeValue;
  228. $releaseData = array(
  229. 'dist' => array(
  230. 'type' => 'pear',
  231. 'url' => $packageUrl . '-' . $version . '.tgz'
  232. ),
  233. 'version' => $version
  234. );
  235. if (isset($depsData[$version])) {
  236. $releaseData += $depsData[$version];
  237. }
  238. try {
  239. $this->addPackage(
  240. $loader->load($packageData + $releaseData)
  241. );
  242. } catch (\UnexpectedValueException $e) {
  243. continue;
  244. }
  245. }
  246. }
  247. }
  248. /**
  249. * @param string $url
  250. * @return DOMDocument
  251. */
  252. private function requestXml($url)
  253. {
  254. $content = file_get_contents($url, false, $this->streamContext);
  255. if (!$content) {
  256. throw new \UnexpectedValueException('The PEAR channel at '.$url.' did not respond.');
  257. }
  258. $dom = new \DOMDocument('1.0', 'UTF-8');
  259. $dom->loadXML($content);
  260. return $dom;
  261. }
  262. }