<?php
// /* vim: set expandtab tabstop=4 shiftwidth=4: */
/**
 * PEAR_Command_Channels (list-channels, update-channels, channel-delete, channel-add,
 * channel-update, channel-info, channel-alias, channel-discover commands)
 *
 * PHP versions 4 and 5
 *
 * LICENSE: This source file is subject to version 3.0 of the PHP license
 * that is available through the world-wide-web at the following URI:
 * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
 * the PHP License and are unable to obtain it through the web, please
 * send a note to license@php.net so we can mail you a copy immediately.
 *
 * @category   pear
 * @package    PEAR
 * @author     Stig Bakken <ssb@php.net>
 * @author     Greg Beaver <cellog@php.net>
 * @copyright  1997-2008 The PHP Group
 * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
 * @version    CVS: $Id: Channels.php,v 1.57 2008/01/03 20:26:36 cellog Exp $
 * @link       http://pear.php.net/package/PEAR
 * @since      File available since Release 1.4.0a1
 */

/**
 * base class
 */
require_once 'PEAR/Command/Common.php';

/**
 * PEAR commands for managing channels.
 *
 * @category   pear
 * @package    PEAR
 * @author     Greg Beaver <cellog@php.net>
 * @copyright  1997-2008 The PHP Group
 * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
 * @version    Release: 1.7.2
 * @link       http://pear.php.net/package/PEAR
 * @since      Class available since Release 1.4.0a1
 */
class PEAR_Command_Channels extends PEAR_Command_Common
{
    // {{{ properties

    var $commands = array(
        'list-channels' => array(
            'summary' => 'List Available Channels',
            'function' => 'doList',
            'shortcut' => 'lc',
            'options' => array(),
            'doc' => '
List all available channels for installation.
',
            ),
        'update-channels' => array(
            'summary' => 'Update the Channel List',
            'function' => 'doUpdateAll',
            'shortcut' => 'uc',
            'options' => array(),
            'doc' => '
List all installed packages in all channels.
'
            ),
        'channel-delete' => array(
            'summary' => 'Remove a Channel From the List',
            'function' => 'doDelete',
            'shortcut' => 'cde',
            'options' => array(),
            'doc' => '<channel name>
Delete a channel from the registry.  You may not
remove any channel that has installed packages.
'
            ),
        'channel-add' => array(
            'summary' => 'Add a Channel',
            'function' => 'doAdd',
            'shortcut' => 'ca',
            'options' => array(),
            'doc' => '<channel.xml>
Add a private channel to the channel list.  Note that all
public channels should be synced using "update-channels".
Parameter may be either a local file or remote URL to a
channel.xml.
'
            ),
        'channel-update' => array(
            'summary' => 'Update an Existing Channel',
            'function' => 'doUpdate',
            'shortcut' => 'cu',
            'options' => array(
                'force' => array(
                    'shortopt' => 'f',
                    'doc' => 'will force download of new channel.xml if an existing channel name is used',
                    ),
                'channel' => array(
                    'shortopt' => 'c',
                    'arg' => 'CHANNEL',
                    'doc' => 'will force download of new channel.xml if an existing channel name is used',
                    ),
),
            'doc' => '[<channel.xml>|<channel name>]
Update a channel in the channel list directly.  Note that all
public channels can be synced using "update-channels".
Parameter may be a local or remote channel.xml, or the name of
an existing channel.
'
            ),
        'channel-info' => array(
            'summary' => 'Retrieve Information on a Channel',
            'function' => 'doInfo',
            'shortcut' => 'ci',
            'options' => array(),
            'doc' => '<package>
List the files in an installed package.
'
            ),
        'channel-alias' => array(
            'summary' => 'Specify an alias to a channel name',
            'function' => 'doAlias',
            'shortcut' => 'cha',
            'options' => array(),
            'doc' => '<channel> <alias>
Specify a specific alias to use for a channel name.
The alias may not be an existing channel name or
alias.
'
            ),
        'channel-discover' => array(
            'summary' => 'Initialize a Channel from its server',
            'function' => 'doDiscover',
            'shortcut' => 'di',
            'options' => array(),
            'doc' => '[<channel.xml>|<channel name>]
Initialize a channel from its server and create a local channel.xml.
If <channel name> is in the format "<username>:<password>@<channel>" then
<username> and <password> will be set as the login username/password for
<channel>. Use caution when passing the username/password in this way, as
it may allow other users on your computer to briefly view your username/
password via the system\'s process list.
'
            ),
        );

    // }}}
    // {{{ constructor

    /**
     * PEAR_Command_Registry constructor.
     *
     * @access public
     */
    function PEAR_Command_Channels(&$ui, &$config)
    {
        parent::PEAR_Command_Common($ui, $config);
    }

    // }}}

    // {{{ doList()
    
    function _sortChannels($a, $b)
    {
        return strnatcasecmp($a->getName(), $b->getName());
    }

    function doList($command, $options, $params)
    {
        $reg = &$this->config->getRegistry();
        $registered = $reg->getChannels();
        usort($registered, array(&$this, '_sortchannels'));
        $i = $j = 0;
        $data = array(
            'caption' => 'Registered Channels:',
            'border' => true,
            'headline' => array('Channel', 'Summary')
            );
        foreach ($registered as $channel) {
            $data['data'][] = array($channel->getName(),
                                      $channel->getSummary());
        }
        if (count($registered)==0) {
            $data = '(no registered channels)';
        }
        $this->ui->outputData($data, $command);
        return true;
    }
    
    function doUpdateAll($command, $options, $params)
    {
        $reg = &$this->config->getRegistry();
        $channels = $reg->getChannels();

        $success = true;
        foreach ($channels as $channel) {
            if ($channel->getName() != '__uri') {
                PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
                $err = $this->doUpdate('channel-update',
                                          $options,
                                          array($channel->getName()));
                if (PEAR::isError($err)) {
                    $this->ui->outputData($err->getMessage(), $command);
                    $success = false;
                } else {
                    $success &= $err;
                }
            }
        }
        return $success;
    }
    
    function doInfo($command, $options, $params)
    {
        if (sizeof($params) != 1) {
            return $this->raiseError("No channel specified");
        }
        $reg = &$this->config->getRegistry();
        $channel = strtolower($params[0]);
        if ($reg->channelExists($channel)) {
            $chan = $reg->getChannel($channel);
            if (PEAR::isError($chan)) {
                return $this->raiseError($chan);
            }
        } else {
            if (strpos($channel, '://')) {
                $downloader = &$this->getDownloader();
                $tmpdir = $this->config->get('temp_dir');
                PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
                $loc = $downloader->downloadHttp($channel, $this->ui, $tmpdir);
                PEAR::staticPopErrorHandling();
                if (PEAR::isError($loc)) {
                    return $this->raiseError('Cannot open "' . $channel .
                        '" (' . $loc->getMessage() . ')');
                } else {
                    $contents = implode('', file($loc));
                }
            } else {
                if (file_exists($params[0])) {
                    $fp = fopen($params[0], 'r');
                    if (!$fp) {
                        return $this->raiseError('Cannot open "' . $params[0] . '"');
                    }
                } else {
                    return $this->raiseError('Unknown channel "' . $channel . '"');
                }
                $contents = '';
                while (!feof($fp)) {
                    $contents .= fread($fp, 1024);
                }
                fclose($fp);
            }
            if (!class_exists('PEAR_ChannelFile')) {
                require_once 'PEAR/ChannelFile.php';
            }
            $chan = new PEAR_ChannelFile;
            $chan->fromXmlString($contents);
            $chan->validate();
            if ($errs = $chan->getErrors(true)) {
                foreach ($errs as $err) {
                    $this->ui->outputData($err['level'] . ': ' . $err['message']);
                }
                return $this->raiseError('Channel file "' . $params[0] . '" is not valid');
            }
        }
        if ($chan) {
            $channel = $chan->getName();
            $caption = 'Channel ' . $channel . ' Information:';
            $data1 = array(
                'caption' => $caption,
                'border' => true);
            $data1['data']['server'] = array('Name and Server', $chan->getName());
            if ($chan->getAlias() != $chan->getName()) {
                $data1['data']['alias'] = array('Alias', $chan->getAlias());
            }
            $data1['data']['summary'] = array('Summary', $chan->getSummary());
            $validate = $chan->getValidationPackage();
            $data1['data']['vpackage'] = array('Validation Package Name', $validate['_content']);
            $data1['data']['vpackageversion'] =
                array('Validation Package Version', $validate['attribs']['version']);
            $d = array();
            $d['main'] = $data1;

            $data['data'] = array();
            $data['caption'] = 'Server Capabilities';
            $data['headline'] = array('Type', 'Version/REST type', 'Function Name/REST base');
            $capabilities = $chan->getFunctions('xmlrpc');
            $soaps = $chan->getFunctions('soap');
            if ($capabilities || $soaps || $chan->supportsREST()) {
                if ($capabilities) {
                    if (!isset($capabilities[0])) {
                        $capabilities = array($capabilities);
                    }
                    foreach ($capabilities as $protocol) {
                        $data['data'][] = array('xmlrpc', $protocol['attribs']['version'],
                            $protocol['_content']);
                    }
                }
                if ($soaps) {
                    if (!isset($soaps[0])) {
                        $soaps = array($soaps);
                    }
                    foreach ($soaps as $protocol) {
                        $data['data'][] = array('soap', $protocol['attribs']['version'],
                            $protocol['_content']);
                    }
                }
                if ($chan->supportsREST()) {
                    $funcs = $chan->getFunctions('rest');
                    if (!isset($funcs[0])) {
                        $funcs = array($funcs);
                    }
                    foreach ($funcs as $protocol) {
                        $data['data'][] = array('rest', $protocol['attribs']['type'],
                            $protocol['_content']); 
                    }
                }
            } else {
                $data['data'][] = array('No supported protocols');
            }
            $d['protocols'] = $data;
            $data['data'] = array();
            $mirrors = $chan->getMirrors();
            if ($mirrors) {
                $data['caption'] = 'Channel ' . $channel . ' Mirrors:';
                unset($data['headline']);
                foreach ($mirrors as $mirror) {
                    $data['data'][] = array($mirror['attribs']['host']);
                    $d['mirrors'] = $data;
                }
                foreach ($mirrors as $i => $mirror) {
                    $data['data'] = array();
                    $data['caption'] = 'Mirror ' . $mirror['attribs']['host'] . ' Capabilities';
                    $data['headline'] = array('Type', 'Version/REST type', 'Function Name/REST base');
                    $capabilities = $chan->getFunctions('xmlrpc', $mirror['attribs']['host']);
                    $soaps = $chan->getFunctions('soap', $mirror['attribs']['host']);
                    if ($capabilities || $soaps || $chan->supportsREST($mirror['attribs']['host'])) {
                        if ($capabilities) {
                            if (!isset($capabilities[0])) {
                                $capabilities = array($capabilities);
                            }
                            foreach ($capabilities as $protocol) {
                                $data['data'][] = array('xmlrpc', $protocol['attribs']['version'],
                                    $protocol['_content']);
                            }
                        }
                        if ($soaps) {
                            if (!isset($soaps[0])) {
                                $soaps = array($soaps);
                            }
                            foreach ($soaps as $protocol) {
                                $data['data'][] = array('soap', $protocol['attribs']['version'],
                                    $protocol['_content']);
                            }
                        }
                        if ($chan->supportsREST($mirror['attribs']['host'])) {
                            $funcs = $chan->getFunctions('rest', $mirror['attribs']['host']);
                            if (!isset($funcs[0])) {
                                $funcs = array($funcs);
                            }
                            foreach ($funcs as $protocol) {
                                $data['data'][] = array('rest', $protocol['attribs']['type'],
                                    $protocol['_content']); 
                            }
                        }
                    } else {
                        $data['data'][] = array('No supported protocols');
                    }
                    $d['mirrorprotocols' . $i] = $data;
                }
            }
            $this->ui->outputData($d, 'channel-info');
        } else {
            return $this->raiseError('Serious error: Channel "' . $params[0] .
                '" has a corrupted registry entry');
        }
    }

    // }}}
    
    function doDelete($command, $options, $params)
    {
        if (sizeof($params) != 1) {
            return $this->raiseError('channel-delete: no channel specified');
        }
        $reg = &$this->config->getRegistry();
        if (!$reg->channelExists($params[0])) {
            return $this->raiseError('channel-delete: channel "' . $params[0] . '" does not exist');
        }
        $channel = $reg->channelName($params[0]);
        if ($channel == 'pear.php.net') {
            return $this->raiseError('Cannot delete the pear.php.net channel');
        }
        if ($channel == 'pecl.php.net') {
            return $this->raiseError('Cannot delete the pecl.php.net channel');
        }
        if ($channel == '__uri') {
            return $this->raiseError('Cannot delete the __uri pseudo-channel');
        }
        if (PEAR::isError($err = $reg->listPackages($channel))) {
            return $err;
        }
        if (count($err)) {
            return $this->raiseError('Channel "' . $channel .
                '" has installed packages, cannot delete');
        }
        if (!$reg->deleteChannel($channel)) {
            return $this->raiseError('Channel "' . $channel . '" deletion failed');
        } else {
            $this->config->deleteChannel($channel);
            $this->ui->outputData('Channel "' . $channel . '" deleted', $command);
        }
    }

    function doAdd($command, $options, $params)
    {
        if (sizeof($params) != 1) {
            return $this->raiseError('channel-add: no channel file specified');
        }
        if (strpos($params[0], '://')) {
            $downloader = &$this->getDownloader();
            $tmpdir = $this->config->get('temp_dir');
            if (!file_exists($tmpdir)) {
                require_once 'System.php';
                PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
                $err = System::mkdir(array('-p', $tmpdir));
                PEAR::staticPopErrorHandling();
                if (PEAR::isError($err)) {
                    return $this->raiseError('channel-add: temp_dir does not exist: "' .
                        $tmpdir . 
                        '" - You can change this location with "pear config-set temp_dir"');
                }
            }
            if (!is_writable($tmpdir)) {
                return $this->raiseError('channel-add: temp_dir is not writable: "' .
                    $tmpdir . 
                    '" - You can change this location with "pear config-set temp_dir"');
            }
            PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
            $loc = $downloader->downloadHttp($params[0], $this->ui, $tmpdir, null, false);
            PEAR::staticPopErrorHandling();
            if (PEAR::isError($loc)) {
                return $this->raiseError('channel-add: Cannot open "' . $params[0] .
                    '" (' . $loc->getMessage() . ')');
            } else {
                list($loc, $lastmodified) = $loc;
                $contents = implode('', file($loc));
            }
        } else {
            $lastmodified = $fp = false;
            if (file_exists($params[0])) {
                $fp = fopen($params[0], 'r');
            }
            if (!$fp) {
                return $this->raiseError('channel-add: cannot open "' . $params[0] . '"');
            }
            $contents = '';
            while (!feof($fp)) {
                $contents .= fread($fp, 1024);
            }
            fclose($fp);
        }
        if (!class_exists('PEAR_ChannelFile')) {
            require_once 'PEAR/ChannelFile.php';
        }
        $channel = new PEAR_ChannelFile;
        PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
        $result = $channel->fromXmlString($contents);
        PEAR::staticPopErrorHandling();
        if (!$result) {
            $exit = false;
            if (count($errors = $channel->getErrors(true))) {
                foreach ($errors as $error) {
                    $this->ui->outputData(ucfirst($error['level'] . ': ' . $error['message']));
                    if (!$exit) {
                        $exit = $error['level'] == 'error' ? true : false;
                    }
                }
                if ($exit) {
                    return $this->raiseError('channel-add: invalid channel.xml file');
                }
            }
        }
        $reg = &$this->config->getRegistry();
        if ($reg->channelExists($channel->getName())) {
            return $this->raiseError('channel-add: Channel "' . $channel->getName() .
                '" exists, use channel-update to update entry');
        }
        $ret = $reg->addChannel($channel, $lastmodified);
        if (PEAR::isError($ret)) {
            return $ret;
        }
        if (!$ret) {
            return $this->raiseError('channel-add: adding Channel "' . $channel->getName() .
                '" to registry failed');
        }
        $this->config->setChannels($reg->listChannels());
        $this->config->writeConfigFile();
        $this->ui->outputData('Adding Channel "' . $channel->getName() . '" succeeded', $command);
    }

    function doUpdate($command, $options, $params)
    {
        $tmpdir = $this->config->get('temp_dir');
        if (!file_exists($tmpdir)) {
            require_once 'System.php';
            PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
            $err = System::mkdir(array('-p', $tmpdir));
            PEAR::staticPopErrorHandling();
            if (PEAR::isError($err)) {
                return $this->raiseError('channel-add: temp_dir does not exist: "' .
                    $tmpdir . 
                    '" - You can change this location with "pear config-set temp_dir"');
            }
        }
        if (!is_writable($tmpdir)) {
            return $this->raiseError('channel-add: temp_dir is not writable: "' .
                $tmpdir . 
                '" - You can change this location with "pear config-set temp_dir"');
        }
        $reg = &$this->config->getRegistry();
        if (sizeof($params) != 1) {
            return $this->raiseError("No channel file specified");
        }
        $lastmodified = false;
        if ((!file_exists($params[0]) || is_dir($params[0]))
              && $reg->channelExists(strtolower($params[0]))) {
            $c = $reg->getChannel(strtolower($params[0]));
            if (PEAR::isError($c)) {
                return $this->raiseError($c);
            }
            $this->ui->outputData("Updating channel \"$params[0]\"", $command);
            $dl = &$this->getDownloader(array());
            // if force is specified, use a timestamp of "1" to force retrieval
            $lastmodified = isset($options['force']) ? false : $c->lastModified();
            PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
            $contents = $dl->downloadHttp('http://' . $c->getName() . '/channel.xml',
                $this->ui, $tmpdir, null, $lastmodified);
            PEAR::staticPopErrorHandling();
            if (PEAR::isError($contents)) {
                return $this->raiseError('Cannot retrieve channel.xml for channel "' .
                    $c->getName() . '" (' . $contents->getMessage() . ')');
            }
            list($contents, $lastmodified) = $contents;
            if (!$contents) {
                $this->ui->outputData("Channel \"$params[0]\" is up to date");
                return;
            }
            $contents = implode('', file($contents));
            if (!class_exists('PEAR_ChannelFile')) {
                require_once 'PEAR/ChannelFile.php';
            }
            $channel = new PEAR_ChannelFile;
            $channel->fromXmlString($contents);
            if (!$channel->getErrors()) {
                // security check: is the downloaded file for the channel we got it from?
                if (strtolower($channel->getName()) != strtolower($c->getName())) {
                    if (isset($options['force'])) {
                        $this->ui->log(0, 'WARNING: downloaded channel definition file' .
                            ' for channel "' . $channel->getName() . '" from channel "' .
                            strtolower($c->getName()) . '"');
                    } else {
                        return $this->raiseError('ERROR: downloaded channel definition file' .
                            ' for channel "' . $channel->getName() . '" from channel "' .
                            strtolower($c->getName()) . '"');
                    }
                }
            }
        } else {
            if (strpos($params[0], '://')) {
                $dl = &$this->getDownloader();
                PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
                $loc = $dl->downloadHttp($params[0],
                    $this->ui, $tmpdir, null, $lastmodified);
                PEAR::staticPopErrorHandling();
                if (PEAR::isError($loc)) {
                    return $this->raiseError("Cannot open " . $params[0] .
                         ' (' . $loc->getMessage() . ')');
                } else {
                    list($loc, $lastmodified) = $loc;
                    $contents = implode('', file($loc));
                }
            } else {
                $fp = false;
                if (file_exists($params[0])) {
                    $fp = fopen($params[0], 'r');
                }
                if (!$fp) {
                    return $this->raiseError("Cannot open " . $params[0]);
                }
                $contents = '';
                while (!feof($fp)) {
                    $contents .= fread($fp, 1024);
                }
                fclose($fp);
            }
            if (!class_exists('PEAR_ChannelFile')) {
                require_once 'PEAR/ChannelFile.php';
            }
            $channel = new PEAR_ChannelFile;
            $channel->fromXmlString($contents);
        }
        $exit = false;
        if (count($errors = $channel->getErrors(true))) {
            foreach ($errors as $error) {
                $this->ui->outputData(ucfirst($error['level'] . ': ' . $error['message']));
                if (!$exit) {
                    $exit = $error['level'] == 'error' ? true : false;
                }
            }
            if ($exit) {
                return $this->raiseError('Invalid channel.xml file');
            }
        }
        if (!$reg->channelExists($channel->getName())) {
            return $this->raiseError('Error: Channel "' . $channel->getName() .
                '" does not exist, use channel-add to add an entry');
        }
        $ret = $reg->updateChannel($channel, $lastmodified);
        if (PEAR::isError($ret)) {
            return $ret;
        }
        if (!$ret) {
            return $this->raiseError('Updating Channel "' . $channel->getName() .
                '" in registry failed');
        }
        $this->config->setChannels($reg->listChannels());
        $this->config->writeConfigFile();
        $this->ui->outputData('Update of Channel "' . $channel->getName() . '" succeeded');
    }

    function &getDownloader()
    {
        if (!class_exists('PEAR_Downloader')) {
            require_once 'PEAR/Downloader.php';
        }
        $a = new PEAR_Downloader($this->ui, array(), $this->config);
        return $a;
    }

    function doAlias($command, $options, $params)
    {
        $reg = &$this->config->getRegistry();
        if (sizeof($params) == 1) {
            return $this->raiseError('No channel alias specified');
        }
        if (sizeof($params) != 2) {
            return $this->raiseError(
                'Invalid format, correct is: channel-alias channel alias');
        }
        if (!$reg->channelExists($params[0], true)) {
            if ($reg->isAlias($params[0])) {
                $extra = ' (use "channel-alias ' . $reg->channelName($params[0]) . ' ' .
                    strtolower($params[1]) . '")';
            } else {
                $extra = '';
            }
            return $this->raiseError('"' . $params[0] . '" is not a valid channel' . $extra);
        }
        if ($reg->isAlias($params[1])) {
            return $this->raiseError('Channel "' . $reg->channelName($params[1]) . '" is ' .
                'already aliased to "' . strtolower($params[1]) . '", cannot re-alias');
        }
        $chan = &$reg->getChannel($params[0]);
        if (PEAR::isError($chan)) {
            return $this->raiseError('Corrupt registry?  Error retrieving channel "' . $params[0] .
                '" information (' . $chan->getMessage() . ')');
        }
        // make it a local alias
        if (!$chan->setAlias(strtolower($params[1]), true)) {
            return $this->raiseError('Alias "' . strtolower($params[1]) .
                '" is not a valid channel alias');
        }
        $reg->updateChannel($chan);
        $this->ui->outputData('Channel "' . $chan->getName() . '" aliased successfully to "' .
            strtolower($params[1]) . '"');
    }

    /**
     * The channel-discover command
     *
     * @param string $command command name
     * @param array  $options option_name => value
     * @param array  $params  list of additional parameters.
     *               $params[0] should contain a string with either:
     *               - <channel name> or
     *               - <username>:<password>@<channel name>
     * @return null|PEAR_Error
     */
    function doDiscover($command, $options, $params)
    {
        $reg = &$this->config->getRegistry();
        if (sizeof($params) != 1) {
            return $this->raiseError("No channel server specified");
        }
        
        // Look for the possible input format "<username>:<password>@<channel>"
        if (preg_match('/^(.+):(.+)@(.+)\\z/', $params[0], $matches)) {
            $username = $matches[1];
            $password = $matches[2];
            $channel = $matches[3];
        } else {
            $channel = $params[0];
        }
        
        if ($reg->channelExists($channel)) {
            if ($reg->isAlias($channel)) {
                return $this->raiseError("A channel alias named \"$channel\" " .
                    'already exists, aliasing channel "' . $reg->channelName($channel)
                    . '"');
            } else {
                return $this->raiseError("Channel \"$channel\" is already initialized");
            }
        }
        $this->pushErrorHandling(PEAR_ERROR_RETURN);
        $err = $this->doAdd($command, $options, array('http://' . $channel . '/channel.xml'));
        $this->popErrorHandling();
        if (PEAR::isError($err)) {
            return $this->raiseError("Discovery of channel \"$channel\" failed (" .
                $err->getMessage() . ')');
        }
        
        // Store username/password if they were given
        // Arguably we should do a logintest on the channel here, but since
        // that's awkward on a REST-based channel (even "pear login" doesn't
        // do it for those), and XML-RPC is deprecated, it's fairly pointless.
        if (isset($username)) {
            $this->config->set('username', $username, 'user', $channel);
            $this->config->set('password', $password, 'user', $channel);
            $this->config->store();
            $this->ui->outputData("Stored login for channel \"$channel\" using username \"$username\"", $command);
        }
        
        $this->ui->outputData("Discovery of channel \"$channel\" succeeded", $command);
    }
}
