PearRepository.php 14 KB

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