3 * PEAR_Command_Remote (remote-info, list-upgrades, remote-list, search, list-all, download,
4 * clear-cache commands)
10 * @author Stig Bakken <ssb@php.net>
11 * @author Greg Beaver <cellog@php.net>
12 * @copyright 1997-2009 The Authors
13 * @license http://opensource.org/licenses/bsd-license.php New BSD License
14 * @link http://pear.php.net/package/PEAR
15 * @since File available since Release 0.1
21 require_once 'PEAR/Command/Common.php';
22 require_once 'PEAR/REST.php';
25 * PEAR commands for remote server querying
29 * @author Stig Bakken <ssb@php.net>
30 * @author Greg Beaver <cellog@php.net>
31 * @copyright 1997-2009 The Authors
32 * @license http://opensource.org/licenses/bsd-license.php New BSD License
33 * @version Release: 1.10.5
34 * @link http://pear.php.net/package/PEAR
35 * @since Class available since Release 0.1
37 class PEAR_Command_Remote extends PEAR_Command_Common
39 var $commands = array(
40 'remote-info' => array(
41 'summary' => 'Information About Remote Packages',
42 'function' => 'doRemoteInfo',
46 Get details on a package from the server.',
48 'list-upgrades' => array(
49 'summary' => 'List Available Upgrades',
50 'function' => 'doListUpgrades',
53 'channelinfo' => array(
55 'doc' => 'output fully channel-aware data, even on failure',
58 'doc' => '[preferred_state]
59 List releases on the server of packages you have installed where
60 a newer version is available with the same release state (stable etc.)
61 or the state passed as the second parameter.'
63 'remote-list' => array(
64 'summary' => 'List Remote Packages',
65 'function' => 'doRemoteList',
71 'doc' => 'specify a channel other than the default channel',
76 Lists the packages available on the configured server along with the
77 latest stable release of each package.',
80 'summary' => 'Search remote package database',
81 'function' => 'doSearch',
87 'doc' => 'specify a channel other than the default channel',
90 'allchannels' => array(
92 'doc' => 'search packages from all known channels',
94 'channelinfo' => array(
96 'doc' => 'output fully channel-aware data, even on failure',
99 'doc' => '[packagename] [packageinfo]
100 Lists all packages which match the search parameters. The first
101 parameter is a fragment of a packagename. The default channel
102 will be used unless explicitly overridden. The second parameter
103 will be used to match any portion of the summary/description',
106 'summary' => 'List All Packages',
107 'function' => 'doListAll',
113 'doc' => 'specify a channel other than the default channel',
116 'channelinfo' => array(
118 'doc' => 'output fully channel-aware data, even on failure',
122 Lists the packages available on the configured server along with the
123 latest stable release of each package.',
126 'summary' => 'Download Package',
127 'function' => 'doDownload',
130 'nocompress' => array(
132 'doc' => 'download an uncompressed (.tar) file',
135 'doc' => '<package>...
136 Download package tarballs. The files will be named as suggested by the
137 server, for example if you download the DB package and the latest stable
138 version of DB is 1.6.5, the downloaded file will be DB-1.6.5.tgz.',
140 'clear-cache' => array(
141 'summary' => 'Clear Web Services Cache',
142 'function' => 'doClearCache',
144 'options' => array(),
146 Clear the REST cache. See also the cache_ttl configuration
153 * PEAR_Command_Remote constructor.
157 function __construct(&$ui, &$config)
159 parent::__construct($ui, $config);
162 function _checkChannelForStatus($channel, $chan)
164 if (PEAR::isError($chan)) {
165 $this->raiseError($chan);
167 if (!is_a($chan, 'PEAR_ChannelFile')) {
168 return $this->raiseError('Internal corruption error: invalid channel "' .
171 $rest = new PEAR_REST($this->config);
172 PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
173 $mirror = $this->config->get('preferred_mirror', null,
175 $a = $rest->downloadHttp('http://' . $channel .
176 '/channel.xml', $chan->lastModified());
177 PEAR::staticPopErrorHandling();
178 if (!PEAR::isError($a) && $a) {
179 $this->ui->outputData('WARNING: channel "' . $channel . '" has ' .
180 'updated its protocols, use "' . PEAR_RUNTYPE . ' channel-update ' . $channel .
185 function doRemoteInfo($command, $options, $params)
187 if (sizeof($params) != 1) {
188 return $this->raiseError("$command expects one param: the remote package name");
190 $savechannel = $channel = $this->config->get('default_channel');
191 $reg = &$this->config->getRegistry();
192 $package = $params[0];
193 $parsed = $reg->parsePackageName($package, $channel);
194 if (PEAR::isError($parsed)) {
195 return $this->raiseError('Invalid package name "' . $package . '"');
198 $channel = $parsed['channel'];
199 $this->config->set('default_channel', $channel);
200 $chan = $reg->getChannel($channel);
201 if (PEAR::isError($e = $this->_checkChannelForStatus($channel, $chan))) {
205 $mirror = $this->config->get('preferred_mirror');
206 if ($chan->supportsREST($mirror) && $base = $chan->getBaseURL('REST1.0', $mirror)) {
207 $rest = &$this->config->getREST('1.0', array());
208 $info = $rest->packageInfo($base, $parsed['package'], $channel);
212 return $this->raiseError('No supported protocol was found');
215 if (PEAR::isError($info)) {
216 $this->config->set('default_channel', $savechannel);
217 return $this->raiseError($info);
220 if (!isset($info['name'])) {
221 return $this->raiseError('No remote package "' . $package . '" was found');
224 $installed = $reg->packageInfo($info['name'], null, $channel);
225 $info['installed'] = $installed['version'] ? $installed['version'] : '- no -';
226 if (is_array($info['installed'])) {
227 $info['installed'] = $info['installed']['release'];
230 $this->ui->outputData($info, $command);
231 $this->config->set('default_channel', $savechannel);
236 function doRemoteList($command, $options, $params)
238 $savechannel = $channel = $this->config->get('default_channel');
239 $reg = &$this->config->getRegistry();
240 if (isset($options['channel'])) {
241 $channel = $options['channel'];
242 if (!$reg->channelExists($channel)) {
243 return $this->raiseError('Channel "' . $channel . '" does not exist');
246 $this->config->set('default_channel', $channel);
249 $chan = $reg->getChannel($channel);
250 if (PEAR::isError($e = $this->_checkChannelForStatus($channel, $chan))) {
254 $list_options = false;
255 if ($this->config->get('preferred_state') == 'stable') {
256 $list_options = true;
259 $available = array();
260 if ($chan->supportsREST($this->config->get('preferred_mirror')) &&
261 $base = $chan->getBaseURL('REST1.1', $this->config->get('preferred_mirror'))
263 // use faster list-all if available
264 $rest = &$this->config->getREST('1.1', array());
265 $available = $rest->listAll($base, $list_options, true, false, false, $chan->getName());
266 } elseif ($chan->supportsREST($this->config->get('preferred_mirror')) &&
267 $base = $chan->getBaseURL('REST1.0', $this->config->get('preferred_mirror'))) {
268 $rest = &$this->config->getREST('1.0', array());
269 $available = $rest->listAll($base, $list_options, true, false, false, $chan->getName());
272 if (PEAR::isError($available)) {
273 $this->config->set('default_channel', $savechannel);
274 return $this->raiseError($available);
279 'caption' => 'Channel ' . $channel . ' Available packages:',
281 'headline' => array('Package', 'Version'),
282 'channel' => $channel
285 if (count($available) == 0) {
286 $data = '(no packages available yet)';
288 foreach ($available as $name => $info) {
289 $version = (isset($info['stable']) && $info['stable']) ? $info['stable'] : '-n/a-';
290 $data['data'][] = array($name, $version);
293 $this->ui->outputData($data, $command);
294 $this->config->set('default_channel', $savechannel);
298 function doListAll($command, $options, $params)
300 $savechannel = $channel = $this->config->get('default_channel');
301 $reg = &$this->config->getRegistry();
302 if (isset($options['channel'])) {
303 $channel = $options['channel'];
304 if (!$reg->channelExists($channel)) {
305 return $this->raiseError("Channel \"$channel\" does not exist");
308 $this->config->set('default_channel', $channel);
311 $list_options = false;
312 if ($this->config->get('preferred_state') == 'stable') {
313 $list_options = true;
316 $chan = $reg->getChannel($channel);
317 if (PEAR::isError($e = $this->_checkChannelForStatus($channel, $chan))) {
321 if ($chan->supportsREST($this->config->get('preferred_mirror')) &&
322 $base = $chan->getBaseURL('REST1.1', $this->config->get('preferred_mirror'))) {
323 // use faster list-all if available
324 $rest = &$this->config->getREST('1.1', array());
325 $available = $rest->listAll($base, $list_options, false, false, false, $chan->getName());
326 } elseif ($chan->supportsREST($this->config->get('preferred_mirror')) &&
327 $base = $chan->getBaseURL('REST1.0', $this->config->get('preferred_mirror'))) {
328 $rest = &$this->config->getREST('1.0', array());
329 $available = $rest->listAll($base, $list_options, false, false, false, $chan->getName());
332 if (PEAR::isError($available)) {
333 $this->config->set('default_channel', $savechannel);
334 return $this->raiseError('The package list could not be fetched from the remote server. Please try again. (Debug info: "' . $available->getMessage() . '")');
338 'caption' => 'All packages [Channel ' . $channel . ']:',
340 'headline' => array('Package', 'Latest', 'Local'),
341 'channel' => $channel,
344 if (isset($options['channelinfo'])) {
345 // add full channelinfo
346 $data['caption'] = 'Channel ' . $channel . ' All packages:';
347 $data['headline'] = array('Channel', 'Package', 'Latest', 'Local',
348 'Description', 'Dependencies');
350 $local_pkgs = $reg->listPackages($channel);
352 foreach ($available as $name => $info) {
353 $installed = $reg->packageInfo($name, null, $channel);
354 if (is_array($installed['version'])) {
355 $installed['version'] = $installed['version']['release'];
357 $desc = $info['summary'];
358 if (isset($params[$name])) {
359 $desc .= "\n\n".$info['description'];
361 if (isset($options['mode']))
363 if ($options['mode'] == 'installed' && !isset($installed['version'])) {
366 if ($options['mode'] == 'notinstalled' && isset($installed['version'])) {
369 if ($options['mode'] == 'upgrades'
370 && (!isset($installed['version']) || version_compare($installed['version'],
371 $info['stable'], '>='))) {
375 $pos = array_search(strtolower($name), $local_pkgs);
376 if ($pos !== false) {
377 unset($local_pkgs[$pos]);
380 if (isset($info['stable']) && !$info['stable']) {
381 $info['stable'] = null;
384 if (isset($options['channelinfo'])) {
385 // add full channelinfo
386 if ($info['stable'] === $info['unstable']) {
387 $state = $info['state'];
391 $latest = $info['stable'].' ('.$state.')';
393 if (isset($installed['version'])) {
394 $inst_state = $reg->packageInfo($name, 'release_state', $channel);
395 $local = $installed['version'].' ('.$inst_state.')';
398 $packageinfo = array(
403 isset($desc) ? $desc : null,
404 isset($info['deps']) ? $info['deps'] : null,
407 $packageinfo = array(
408 $reg->channelAlias($channel) . '/' . $name,
409 isset($info['stable']) ? $info['stable'] : null,
410 isset($installed['version']) ? $installed['version'] : null,
411 isset($desc) ? $desc : null,
412 isset($info['deps']) ? $info['deps'] : null,
415 $data['data'][$info['category']][] = $packageinfo;
418 if (isset($options['mode']) && in_array($options['mode'], array('notinstalled', 'upgrades'))) {
419 $this->config->set('default_channel', $savechannel);
420 $this->ui->outputData($data, $command);
424 foreach ($local_pkgs as $name) {
425 $info = &$reg->getPackage($name, $channel);
426 $data['data']['Local'][] = array(
427 $reg->channelAlias($channel) . '/' . $info->getPackage(),
435 $this->config->set('default_channel', $savechannel);
436 $this->ui->outputData($data, $command);
440 function doSearch($command, $options, $params)
442 if ((!isset($params[0]) || empty($params[0]))
443 && (!isset($params[1]) || empty($params[1])))
445 return $this->raiseError('no valid search string supplied');
448 $channelinfo = isset($options['channelinfo']);
449 $reg = &$this->config->getRegistry();
450 if (isset($options['allchannels'])) {
451 // search all channels
452 unset($options['allchannels']);
453 $channels = $reg->getChannels();
455 PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
456 foreach ($channels as $channel) {
457 if ($channel->getName() != '__uri') {
458 $options['channel'] = $channel->getName();
459 $ret = $this->doSearch($command, $options, $params);
460 if (PEAR::isError($ret)) {
466 PEAR::staticPopErrorHandling();
467 if (count($errors) !== 0) {
468 // for now, only give first error
469 return PEAR::raiseError($errors[0]);
475 $savechannel = $channel = $this->config->get('default_channel');
476 $package = strtolower($params[0]);
477 $summary = isset($params[1]) ? $params[1] : false;
478 if (isset($options['channel'])) {
479 $reg = &$this->config->getRegistry();
480 $channel = $options['channel'];
481 if (!$reg->channelExists($channel)) {
482 return $this->raiseError('Channel "' . $channel . '" does not exist');
485 $this->config->set('default_channel', $channel);
488 $chan = $reg->getChannel($channel);
489 if (PEAR::isError($e = $this->_checkChannelForStatus($channel, $chan))) {
493 if ($chan->supportsREST($this->config->get('preferred_mirror')) &&
494 $base = $chan->getBaseURL('REST1.0', $this->config->get('preferred_mirror'))) {
495 $rest = &$this->config->getREST('1.0', array());
496 $available = $rest->listAll($base, false, false, $package, $summary, $chan->getName());
499 if (PEAR::isError($available)) {
500 $this->config->set('default_channel', $savechannel);
501 return $this->raiseError($available);
504 if (!$available && !$channelinfo) {
505 // clean exit when not found, no error !
506 $data = 'no packages found that match pattern "' . $package . '", for channel '.$channel.'.';
507 $this->ui->outputData($data);
508 $this->config->set('default_channel', $channel);
514 'caption' => 'Matched packages, channel ' . $channel . ':',
516 'headline' => array('Channel', 'Package', 'Stable/(Latest)', 'Local'),
517 'channel' => $channel
521 'caption' => 'Matched packages, channel ' . $channel . ':',
523 'headline' => array('Package', 'Stable/(Latest)', 'Local'),
524 'channel' => $channel
528 if (!$available && $channelinfo) {
529 unset($data['headline']);
530 $data['data'] = 'No packages found that match pattern "' . $package . '".';
531 $available = array();
534 foreach ($available as $name => $info) {
535 $installed = $reg->packageInfo($name, null, $channel);
536 $desc = $info['summary'];
537 if (isset($params[$name]))
538 $desc .= "\n\n".$info['description'];
540 if (!isset($info['stable']) || !$info['stable']) {
541 $version_remote = 'none';
543 if ($info['unstable']) {
544 $version_remote = $info['unstable'];
546 $version_remote = $info['stable'];
548 $version_remote .= ' ('.$info['state'].')';
550 $version = is_array($installed['version']) ? $installed['version']['release'] :
551 $installed['version'];
553 $packageinfo = array(
561 $packageinfo = array(
568 $data['data'][$info['category']][] = $packageinfo;
571 $this->ui->outputData($data, $command);
572 $this->config->set('default_channel', $channel);
576 function &getDownloader($options)
578 if (!class_exists('PEAR_Downloader')) {
579 require_once 'PEAR/Downloader.php';
581 $a = new PEAR_Downloader($this->ui, $options, $this->config);
585 function doDownload($command, $options, $params)
587 // make certain that dependencies are ignored
588 $options['downloadonly'] = 1;
590 // eliminate error messages for preferred_state-related errors
591 /* TODO: Should be an option, but until now download does respect
593 /* $options['ignorepreferred_state'] = 1; */
594 // eliminate error messages for preferred_state-related errors
596 $downloader = &$this->getDownloader($options);
597 PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
598 $e = $downloader->setDownloadDir(getcwd());
599 PEAR::staticPopErrorHandling();
600 if (PEAR::isError($e)) {
601 return $this->raiseError('Current directory is not writeable, cannot download');
605 $downloaded = array();
606 $err = $downloader->download($params);
607 if (PEAR::isError($err)) {
611 $errors = $downloader->getErrorMsgs();
612 if (count($errors)) {
613 foreach ($errors as $error) {
614 if ($error !== null) {
615 $this->ui->outputData($error);
619 return $this->raiseError("$command failed");
622 $downloaded = $downloader->getDownloadedPackages();
623 foreach ($downloaded as $pkg) {
624 $this->ui->outputData("File $pkg[file] downloaded", $command);
630 function downloadCallback($msg, $params = null)
632 if ($msg == 'done') {
633 $this->bytes_downloaded = $params;
637 function doListUpgrades($command, $options, $params)
639 require_once 'PEAR/Common.php';
640 if (isset($params[0]) && !is_array(PEAR_Common::betterStates($params[0]))) {
641 return $this->raiseError($params[0] . ' is not a valid state (stable/beta/alpha/devel/etc.) try "pear help list-upgrades"');
644 $savechannel = $channel = $this->config->get('default_channel');
645 $reg = &$this->config->getRegistry();
646 foreach ($reg->listChannels() as $channel) {
647 $inst = array_flip($reg->listPackages($channel));
652 if ($channel == '__uri') {
656 $this->config->set('default_channel', $channel);
657 $state = empty($params[0]) ? $this->config->get('preferred_state') : $params[0];
659 $caption = $channel . ' Available Upgrades';
660 $chan = $reg->getChannel($channel);
661 if (PEAR::isError($e = $this->_checkChannelForStatus($channel, $chan))) {
667 $preferred_mirror = $this->config->get('preferred_mirror');
668 if ($chan->supportsREST($preferred_mirror) &&
670 ($base2 = $chan->getBaseURL('REST1.3', $preferred_mirror))
671 || ($base = $chan->getBaseURL('REST1.0', $preferred_mirror))
676 $rest = &$this->config->getREST('1.3', array());
679 $rest = &$this->config->getREST('1.0', array());
682 if (empty($state) || $state == 'any') {
685 $caption .= ' (' . implode(', ', PEAR_Common::betterStates($state, true)) . ')';
688 PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
689 $latest = $rest->listLatestUpgrades($base, $state, $inst, $channel, $reg);
690 PEAR::staticPopErrorHandling();
693 if (PEAR::isError($latest)) {
694 $this->ui->outputData($latest->getMessage());
699 if (PEAR::isError($latest)) {
700 $this->config->set('default_channel', $savechannel);
705 'caption' => $caption,
707 'headline' => array('Channel', 'Package', 'Local', 'Remote', 'Size'),
708 'channel' => $channel
711 foreach ((array)$latest as $pkg => $info) {
712 $package = strtolower($pkg);
713 if (!isset($inst[$package])) {
714 // skip packages we don't have installed
719 $inst_version = $reg->packageInfo($package, 'version', $channel);
720 $inst_state = $reg->packageInfo($package, 'release_state', $channel);
721 if (version_compare("$version", "$inst_version", "le")) {
722 // installed version is up-to-date
726 if ($filesize >= 20480) {
727 $filesize += 1024 - ($filesize % 1024);
728 $fs = sprintf("%dkB", $filesize / 1024);
729 } elseif ($filesize > 0) {
730 $filesize += 103 - ($filesize % 103);
731 $fs = sprintf("%.1fkB", $filesize / 1024.0);
733 $fs = " -"; // XXX center instead
736 $data['data'][] = array($channel, $pkg, "$inst_version ($inst_state)", "$version ($state)", $fs);
739 if (isset($options['channelinfo'])) {
740 if (empty($data['data'])) {
741 unset($data['headline']);
742 if (count($inst) == 0) {
743 $data['data'] = '(no packages installed)';
745 $data['data'] = '(no upgrades available)';
748 $this->ui->outputData($data, $command);
750 if (empty($data['data'])) {
751 $this->ui->outputData('Channel ' . $channel . ': No upgrades available');
753 $this->ui->outputData($data, $command);
758 $this->config->set('default_channel', $savechannel);
762 function doClearCache($command, $options, $params)
764 $cache_dir = $this->config->get('cache_dir');
765 $verbose = $this->config->get('verbose');
767 if (!file_exists($cache_dir) || !is_dir($cache_dir)) {
768 return $this->raiseError("$cache_dir does not exist or is not a directory");
771 if (!($dp = @opendir($cache_dir))) {
772 return $this->raiseError("opendir($cache_dir) failed: $php_errormsg");
776 $output .= "reading directory $cache_dir\n";
780 while ($ent = readdir($dp)) {
781 if (preg_match('/rest.cache(file|id)\\z/', $ent)) {
782 $path = $cache_dir . DIRECTORY_SEPARATOR . $ent;
783 if (file_exists($path)) {
784 $ok = @unlink($path);
792 $output .= "deleted $path\n";
795 } elseif ($verbose >= 1) {
796 $output .= "failed to delete $path $php_errormsg\n";
803 $output .= "$num cache entries cleared\n";
806 $this->ui->outputData(rtrim($output), $command);