PearRepository.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  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\IO\IOInterface;
  13. use Composer\Package\Loader\ArrayLoader;
  14. use Composer\Util\RemoteFilesystem;
  15. use Composer\Json\JsonFile;
  16. use Composer\Config;
  17. use Composer\Downloader\TransportException;
  18. /**
  19. * @author Benjamin Eberlei <kontakt@beberlei.de>
  20. * @author Jordi Boggiano <j.boggiano@seld.be>
  21. */
  22. class PearRepository extends ArrayRepository
  23. {
  24. private static $channelNames = array();
  25. private $url;
  26. private $baseUrl;
  27. private $channel;
  28. private $io;
  29. private $rfs;
  30. public function __construct(array $repoConfig, IOInterface $io, Config $config, RemoteFilesystem $rfs = null)
  31. {
  32. if (!preg_match('{^https?://}', $repoConfig['url'])) {
  33. $repoConfig['url'] = 'http://'.$repoConfig['url'];
  34. }
  35. if (function_exists('filter_var') && !filter_var($repoConfig['url'], FILTER_VALIDATE_URL)) {
  36. throw new \UnexpectedValueException('Invalid url given for PEAR repository: '.$repoConfig['url']);
  37. }
  38. $this->url = rtrim($repoConfig['url'], '/');
  39. $this->channel = !empty($repoConfig['channel']) ? $repoConfig['channel'] : null;
  40. $this->io = $io;
  41. $this->rfs = $rfs ?: new RemoteFilesystem($this->io);
  42. }
  43. protected function initialize()
  44. {
  45. parent::initialize();
  46. $this->io->write('Initializing PEAR repository '.$this->url);
  47. $this->initializeChannel();
  48. $this->io->write('Packages names will be prefixed with: pear-'.$this->channel.'/');
  49. // try to load as a composer repo
  50. try {
  51. $json = new JsonFile($this->url.'/packages.json', new RemoteFilesystem($this->io));
  52. $packages = $json->read();
  53. if ($this->io->isVerbose()) {
  54. $this->io->write('Repository is Composer-compatible, loading via packages.json instead of PEAR protocol');
  55. }
  56. $loader = new ArrayLoader();
  57. foreach ($packages as $data) {
  58. foreach ($data['versions'] as $rev) {
  59. if (strpos($rev['name'], 'pear-'.$this->channel) !== 0) {
  60. $rev['name'] = 'pear-'.$this->channel.'/'.$rev['name'];
  61. }
  62. $this->addPackage($loader->load($rev));
  63. }
  64. }
  65. return;
  66. } catch (\Exception $e) {
  67. }
  68. $this->fetchFromServer();
  69. }
  70. protected function initializeChannel()
  71. {
  72. $channelXML = $this->requestXml($this->url . "/channel.xml");
  73. if (!$this->channel) {
  74. $this->channel = $channelXML->getElementsByTagName("suggestedalias")->item(0)->nodeValue
  75. ?: $channelXML->getElementsByTagName("name")->item(0)->nodeValue;
  76. }
  77. if (!$this->baseUrl) {
  78. $this->baseUrl = $channelXML->getElementsByTagName("baseurl")->item(0)->nodeValue
  79. ? trim($channelXML->getElementsByTagName("baseurl")->item(0)->nodeValue, '/')
  80. : $this->url . '/rest';
  81. }
  82. self::$channelNames[$channelXML->getElementsByTagName("name")->item(0)->nodeValue] = $this->channel;
  83. }
  84. protected function fetchFromServer()
  85. {
  86. $categoryXML = $this->requestXml($this->baseUrl . "/c/categories.xml");
  87. $categories = $categoryXML->getElementsByTagName("c");
  88. foreach ($categories as $category) {
  89. $link = $this->baseUrl . '/c/' . $category->nodeValue;
  90. try {
  91. $packagesLink = $link . "/packagesinfo.xml";
  92. $this->fetchPear2Packages($packagesLink);
  93. } catch (TransportException $e) {
  94. if (false === strpos($e->getMessage(), '404')) {
  95. throw $e;
  96. }
  97. $categoryLink = $link . "/packages.xml";
  98. $this->fetchPearPackages($categoryLink);
  99. }
  100. }
  101. }
  102. /**
  103. * @param string $categoryLink
  104. * @throws TransportException
  105. * @throws InvalidArgumentException
  106. */
  107. private function fetchPearPackages($categoryLink)
  108. {
  109. $packagesXML = $this->requestXml($categoryLink);
  110. $packages = $packagesXML->getElementsByTagName('p');
  111. $loader = new ArrayLoader();
  112. foreach ($packages as $package) {
  113. $packageName = $package->nodeValue;
  114. $fullName = 'pear-'.$this->channel.'/'.$packageName;
  115. $releaseLink = $this->baseUrl . "/r/" . $packageName;
  116. $allReleasesLink = $releaseLink . "/allreleases2.xml";
  117. try {
  118. $releasesXML = $this->requestXml($allReleasesLink);
  119. } catch (TransportException $e) {
  120. if (strpos($e->getMessage(), '404')) {
  121. continue;
  122. }
  123. throw $e;
  124. }
  125. $releases = $releasesXML->getElementsByTagName('r');
  126. foreach ($releases as $release) {
  127. /* @var $release \DOMElement */
  128. $pearVersion = $release->getElementsByTagName('v')->item(0)->nodeValue;
  129. $packageData = array(
  130. 'name' => $fullName,
  131. 'type' => 'library',
  132. 'dist' => array('type' => 'pear', 'url' => $this->url.'/get/'.$packageName.'-'.$pearVersion.".tgz"),
  133. 'version' => $pearVersion,
  134. 'autoload' => array(
  135. 'classmap' => array(''),
  136. ),
  137. );
  138. try {
  139. $deps = $this->rfs->getContents($this->url, $releaseLink . "/deps.".$pearVersion.".txt", false);
  140. } catch (TransportException $e) {
  141. if (strpos($e->getMessage(), '404')) {
  142. continue;
  143. }
  144. throw $e;
  145. }
  146. $packageData += $this->parseDependencies($deps);
  147. try {
  148. $this->addPackage($loader->load($packageData));
  149. if ($this->io->isVerbose()) {
  150. $this->io->write('Loaded '.$packageData['name'].' '.$packageData['version']);
  151. }
  152. } catch (\UnexpectedValueException $e) {
  153. if ($this->io->isVerbose()) {
  154. $this->io->write('Could not load '.$packageData['name'].' '.$packageData['version'].': '.$e->getMessage());
  155. }
  156. continue;
  157. }
  158. }
  159. }
  160. }
  161. /**
  162. * @param array $data
  163. * @return string
  164. */
  165. private function parseVersion(array $data)
  166. {
  167. if (!isset($data['min']) && !isset($data['max'])) {
  168. return '*';
  169. }
  170. $versions = array();
  171. if (isset($data['min'])) {
  172. $versions[] = '>=' . $data['min'];
  173. }
  174. if (isset($data['max'])) {
  175. $versions[] = '<=' . $data['max'];
  176. }
  177. return implode(',', $versions);
  178. }
  179. /**
  180. * @todo Improve dependencies resolution of pear packages.
  181. * @param array $options
  182. * @return array
  183. */
  184. private function parseDependenciesOptions(array $depsOptions)
  185. {
  186. $data = array();
  187. foreach ($depsOptions as $name => $options) {
  188. // make sure single deps are wrapped in an array
  189. if (isset($options['name'])) {
  190. $options = array($options);
  191. }
  192. if ('php' == $name) {
  193. $data[$name] = $this->parseVersion($options);
  194. } elseif ('package' == $name) {
  195. foreach ($options as $key => $value) {
  196. if (isset($value['providesextension'])) {
  197. // skip PECL dependencies
  198. continue;
  199. }
  200. if (isset($value['uri'])) {
  201. // skip uri-based dependencies
  202. continue;
  203. }
  204. if (is_array($value)) {
  205. $dataKey = $value['name'];
  206. if (false === strpos($dataKey, '/')) {
  207. $dataKey = $this->getChannelShorthand($value['channel']).'/'.$dataKey;
  208. }
  209. $data['pear-'.$dataKey] = $this->parseVersion($value);
  210. }
  211. }
  212. } elseif ('extension' == $name) {
  213. foreach ($options as $key => $value) {
  214. $dataKey = 'ext-' . $value['name'];
  215. $data[$dataKey] = $this->parseVersion($value);
  216. }
  217. }
  218. }
  219. return $data;
  220. }
  221. /**
  222. * @param string $deps
  223. * @return array
  224. * @throws InvalidArgumentException
  225. */
  226. private function parseDependencies($deps)
  227. {
  228. if (preg_match('((O:([0-9])+:"([^"]+)"))', $deps, $matches)) {
  229. if (strlen($matches[3]) == $matches[2]) {
  230. throw new \InvalidArgumentException("Invalid dependency data, it contains serialized objects.");
  231. }
  232. }
  233. $deps = (array) @unserialize($deps);
  234. unset($deps['required']['pearinstaller']);
  235. $depsData = array();
  236. if (!empty($deps['required'])) {
  237. $depsData['require'] = $this->parseDependenciesOptions($deps['required']);
  238. }
  239. if (!empty($deps['optional'])) {
  240. $depsData['suggest'] = $this->parseDependenciesOptions($deps['optional']);
  241. }
  242. return $depsData;
  243. }
  244. /**
  245. * @param string $packagesLink
  246. * @return void
  247. * @throws InvalidArgumentException
  248. */
  249. private function fetchPear2Packages($packagesLink)
  250. {
  251. $loader = new ArrayLoader();
  252. $packagesXml = $this->requestXml($packagesLink);
  253. $informations = $packagesXml->getElementsByTagName('pi');
  254. foreach ($informations as $information) {
  255. $package = $information->getElementsByTagName('p')->item(0);
  256. $packageName = $package->getElementsByTagName('n')->item(0)->nodeValue;
  257. $fullName = 'pear-'.$this->channel.'/'.$packageName;
  258. $packageData = array(
  259. 'name' => $fullName,
  260. 'type' => 'library',
  261. 'autoload' => array(
  262. 'classmap' => array(''),
  263. ),
  264. );
  265. $packageKeys = array('l' => 'license', 'd' => 'description');
  266. foreach ($packageKeys as $pear => $composer) {
  267. if ($package->getElementsByTagName($pear)->length > 0
  268. && ($pear = $package->getElementsByTagName($pear)->item(0)->nodeValue)) {
  269. $packageData[$composer] = $pear;
  270. }
  271. }
  272. $depsData = array();
  273. foreach ($information->getElementsByTagName('deps') as $depElement) {
  274. $depsVersion = $depElement->getElementsByTagName('v')->item(0)->nodeValue;
  275. $depsData[$depsVersion] = $this->parseDependencies(
  276. $depElement->getElementsByTagName('d')->item(0)->nodeValue
  277. );
  278. }
  279. $releases = $information->getElementsByTagName('a')->item(0);
  280. if (!$releases) {
  281. continue;
  282. }
  283. $releases = $releases->getElementsByTagName('r');
  284. $packageUrl = $this->url . '/get/' . $packageName;
  285. foreach ($releases as $release) {
  286. $version = $release->getElementsByTagName('v')->item(0)->nodeValue;
  287. $releaseData = array(
  288. 'dist' => array(
  289. 'type' => 'pear',
  290. 'url' => $packageUrl . '-' . $version . '.tgz'
  291. ),
  292. 'version' => $version
  293. );
  294. if (isset($depsData[$version])) {
  295. $releaseData += $depsData[$version];
  296. }
  297. $package = $packageData + $releaseData;
  298. try {
  299. $this->addPackage($loader->load($package));
  300. if ($this->io->isVerbose()) {
  301. $this->io->write('Loaded '.$package['name'].' '.$package['version']);
  302. }
  303. } catch (\UnexpectedValueException $e) {
  304. if ($this->io->isVerbose()) {
  305. $this->io->write('Could not load '.$package['name'].' '.$package['version'].': '.$e->getMessage());
  306. }
  307. continue;
  308. }
  309. }
  310. }
  311. }
  312. /**
  313. * @param string $url
  314. * @return DOMDocument
  315. */
  316. private function requestXml($url)
  317. {
  318. $content = $this->rfs->getContents($this->url, $url, false);
  319. if (!$content) {
  320. throw new \UnexpectedValueException('The PEAR channel at '.$url.' did not respond.');
  321. }
  322. $dom = new \DOMDocument('1.0', 'UTF-8');
  323. $dom->loadXML($content, LIBXML_NOERROR);
  324. return $dom;
  325. }
  326. private function getChannelShorthand($url)
  327. {
  328. if (!isset(self::$channelNames[$url])) {
  329. try {
  330. $channelXML = $this->requestXml('http://'.$url."/channel.xml");
  331. $shorthand = $channelXML->getElementsByTagName("suggestedalias")->item(0)->nodeValue
  332. ?: $channelXML->getElementsByTagName("name")->item(0)->nodeValue;
  333. self::$channelNames[$url] = $shorthand;
  334. } catch (\Exception $e) {
  335. self::$channelNames[$url] = substr($url, 0, strpos($url, '.'));
  336. }
  337. }
  338. return self::$channelNames[$url];
  339. }
  340. }