<?php
/**
 * The RPC:: class provides a set of server and client methods for
 * RPC communication.
 *
 * TODO:
 * - Introspection documentation and method signatures.
 *
 * EXAMPLE:
 * $response = RPC::request('xmlrpc', '/horde/rpc.php', 'www.example.com', 80, 'contacts.search',
 *                          array(array('jan'), array('localsql'), array('name', 'email')),
 *                          Auth::getAuth(), Auth::getCredential('password'));
 *
 * $Horde: horde/lib/RPC.php,v 1.21 2003/08/05 01:21:44 chuck Exp $
 *
 * Copyright 2002-2003 Jan Schneider <jan@horde.org>
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
 *
 * @author  Jan Schneider <jan@horde.org>
 * @version $Revision: 1.21 $
 * @since   Horde 3.0
 * @package Horde_RPC
 */
class RPC {

    /**
     * Attempts to return a concrete RPC server instance based on $driver.
     *
     * @access public
     *
     * @param mixed $driver           The type of concrete Auth subclass to
     *                                return. This is based on the protocol
     *                                driver ($driver). The code is dynamically
     *                                included. If $driver is an array, then we
     *                                will look in $driver[0]/lib/Auth/ for
     *                                the subclass implementation named
     *                                $driver[1].php.
     * @param optional array $params  A hash containing any additional
     *                                configuration or connection parameters a
     *                                subclass might need.
     *
     * @return object RPC    The newly created concrete RPC server instance, or
     *                       a PEAR_Error on an error.
     */
    function &factory($driver, $params = null)
    {
        if (is_array($driver)) {
            list($app, $driver) = $driver;
        }

        $driver = basename($driver);

        if (!empty($app)) {
            require_once $GLOBALS['registry']->getParam('fileroot', $app) . '/lib/RPC/' . $driver . '.php';
        } elseif (@file_exists(dirname(__FILE__) . '/RPC/' . $driver . '.php')) {
            require_once dirname(__FILE__) . '/RPC/' . $driver . '.php';
        } else {
            @include_once 'Horde/RPC/' . $driver . '.php';
        }
        $class = 'RPC_' . $driver;
        if (class_exists($class)) {
            return new $class($params);
        } else {
            require_once 'PEAR.php';
            return PEAR::raiseError('Class definition of ' . $class . ' not found.');
        }
    }

    /**
     * Attempts to return a reference to a concrete RPC server instance
     * based on $driver. It will only create a new instance if no RPC server
     * instance with the same parameters currently exists.
     *
     * This should be used if multiple RPC servers (and thus, multiple RPC
     * server instances) are required.
     *
     * This method must be invoked as: $var = &RPC::singleton()
     *
     * @access public
     *
     * @param string $driver          The type of concrete RPC subclass to
     *                                return. This is based on the protocol
     *                                driver ($driver). The code is dynamically
     *                                included.
     * @param optional array $params  A hash containing any additional
     *                                configuration or connection parameters a
     *                                subclass might need.
     *
     * @return object RPC   The concrete RPC server reference, or a PEAR_Error
     *                      on an error.
     */
    function &singleton($driver, $params = null)
    {
        static $instances;

        if (!isset($instances)) {
            $instances = array();
        }

        $signature = serialize(array($driver, $params));
        if (!array_key_exists($signature, $instances)) {
            $instances[$signature] = &RPC::factory($driver, $params);
        }

        return $instances[$signature];
    }

    /**
     * RPC server constructor
     *
     * @access private
     * @return object   An RPC server instance.
     */
    function RPC()
    {
        register_shutdown_function(array($this, 'shutdown'));
    }

    /**
     * Cleans up the RPC server.
     */
    function shutdown()
    {
    }

    /**
     * Sends an RPC request to the server and returns the result.
     *
     * @param string    The raw request string.
     *
     * @return string   The XML encoded response from the server.
     */
    function getResponse($request)
    {
        return _("not implemented");
    }

    /**
     * Get the Content-Type of the response.
     *
     * @return string  The MIME Content-Type of the RPC response.
     */
    function getResponseContentType()
    {
        return 'text/xml';
    }

    /**
     * Will be registered as the handler for all available methods
     * and will call the appropriate function through the registry.
     *
     * @access private
     *
     * @param string $method    The name of the method called by the RPC request.
     * @param array $params     The passed parameters.
     * @param mixed $data       Unknown.
     *
     * @return mixed            The result of the called registry method.
     */
    function _dispatcher($method, $params, $data)
    {
        global $registry;
        $method = str_replace('.', '/', $method);

        if (!$registry->hasMethod($method)) {
            return sprintf(_("Method '%s' is not defined"), $method);
        }

        return $registry->call($method, $params);
    }

    /**
     * Builds an RPC request and sends it to the RPC server.
     *
     * This statically called method is actually the RPC client.
     *
     * @param string $driver    The protocol driver to use. Currently 'soap' and
     *                          'xmlrpc' are available.
     * @param string $url       The path to the RPC server on the called host.
     * @param string $method    The method to call.
     * @param array $params     (optional) A hash containing any necessary
     *                          parameters for the method call.
     * @param $options  Optional associative array of parameters depending on
     *                  the selected protocol driver.
     *
     * @return mixed            The returned result from the method or a PEAR
     *                          error object on failure.
     */
    function request($driver, $url, $method, $params = null, $options = array())
    {
        if (is_array($driver)) {
            list($app, $driver) = $driver;
        }

        $driver = basename($driver);

        if (!empty($app)) {
            require_once $GLOBALS['registry']->getParam('fileroot', $app) . '/lib/RPC/' . $driver . '.php';
        } elseif (@file_exists(dirname(__FILE__) . '/RPC/' . $driver . '.php')) {
            require_once dirname(__FILE__) . '/RPC/' . $driver . '.php';
        } else {
            @include_once 'Horde/RPC/' . $driver . '.php';
        }
        $class = 'RPC_' . $driver;
        if (class_exists($class)) {
            return call_user_func(array($class, 'request'), $url, $method, $params, $options);
        } else {
            require_once 'PEAR.php';
            return PEAR::raiseError('Class definition of ' . $class . ' not found.');
        }
    }

    /**
     * Parses a string to an associative array just like parse_url() but makes
     * sure that all URL parts necessary for RPC::request() are available.
     *
     * @param string $url   The URL to be parsed.
     * @return array        The associative array with all URL parts.
     * @see request()
     */
    function parseUrl($url)
    {
        $url = parse_url($url);
        if (!array_key_exists('path', $url)) {
            $url['path'] = '/';
        }
        if (!array_key_exists('host', $url)) {
            list($url['host'], $url['path']) = explode('/', $url['path'], 2);
            $url['path'] = '/' . $url['path'];
        }
        if (!array_key_exists('port', $url)) {
            $url['port'] = 80;
        }
        if (array_key_exists('scheme', $url) && $url['scheme'] == 'https') {
            $GLOBALS['notification']->push(_("HTTPS is currently not supported."), 'horde.warning');
        }
        if (substr($url['path'], -1) == '/') {
            $url['path'] = substr($url['path'], 0, -1);
        }
        return $url;
    }

}
