Import RPC from Framework CVS HEAD. This version will be using Horde_Xml_Element...
authorBen Klang <ben@alkaloid.net>
Mon, 6 Apr 2009 03:45:42 +0000 (23:45 -0400)
committerBen Klang <ben@alkaloid.net>
Mon, 6 Apr 2009 03:45:42 +0000 (23:45 -0400)
15 files changed:
framework/RPC/RPC.php [new file with mode: 0644]
framework/RPC/RPC/PhpSoap.php [new file with mode: 0644]
framework/RPC/RPC/jsonrpc.php [new file with mode: 0644]
framework/RPC/RPC/phpgw.php [new file with mode: 0644]
framework/RPC/RPC/soap.php [new file with mode: 0644]
framework/RPC/RPC/syncml.php [new file with mode: 0644]
framework/RPC/RPC/syncml_wbxml.php [new file with mode: 0644]
framework/RPC/RPC/webdav.php [new file with mode: 0644]
framework/RPC/RPC/xmlrpc.php [new file with mode: 0644]
framework/RPC/docs/examples/soap.php [new file with mode: 0644]
framework/RPC/docs/examples/soap.pl [new file with mode: 0644]
framework/RPC/docs/examples/xmlrpc.php [new file with mode: 0644]
framework/RPC/docs/examples/xmlrpc.pl [new file with mode: 0644]
framework/RPC/package.xml [new file with mode: 0644]
framework/RPC/tests/rpc-test.php [new file with mode: 0644]

diff --git a/framework/RPC/RPC.php b/framework/RPC/RPC.php
new file mode 100644 (file)
index 0000000..0aa7721
--- /dev/null
@@ -0,0 +1,226 @@
+<?php
+/**
+ * The Horde_RPC:: class provides a set of server and client methods for
+ * RPC communication.
+ *
+ * TODO:
+ * - Introspection documentation and method signatures.
+ *
+ * EXAMPLE:
+ * <code>
+ * $response = Horde_RPC::request('xmlrpc',
+ *                                'http://localhost:80/horde/rpc.php',
+ *                                'contacts.search',
+ *                                array(array('jan'), array('localsql'),
+ *                                      array('name', 'email')),
+ *                                array('user' => Auth::getAuth(),
+ *                                      'pass' => Auth::getCredential('password')));
+ * </code>
+ *
+ * $Horde: framework/RPC/RPC.php,v 1.30 2009/01/06 17:49:37 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.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>
+ * @since   Horde 3.0
+ * @package Horde_RPC
+ */
+class Horde_RPC {
+
+    /**
+     * All driver-specific parameters.
+     *
+     * @var array
+     */
+    var $_params = array();
+
+    /**
+     * Do we need an authenticated user?
+     *
+     * @var boolean
+     */
+    var $_requireAuthorization = true;
+
+    /**
+     * Whether we should exit if auth fails instead of requesting
+     * authorization credentials.
+     *
+     * @var boolean
+     */
+    var $_requestMissingAuthorization = true;
+
+    /**
+     * RPC server constructor.
+     *
+     * @param array $config  A hash containing any additional configuration or
+     *                       connection parameters a subclass might need.
+     *
+     * @return Horde_RPC  An RPC server instance.
+     */
+    function Horde_RPC($params = array())
+    {
+        $this->_params = $params;
+
+        if (isset($params['requireAuthorization'])) {
+            $this->_requireAuthorization = $params['requireAuthorization'];
+        }
+        if (isset($params['requestMissingAuthorization'])) {
+            $this->_requestMissingAuthorization = $params['requestMissingAuthorization'];
+        }
+    }
+
+    /**
+     * Check authentication. Different backends may handle
+     * authentication in different ways. The base class implementation
+     * checks for HTTP Authentication against the Horde auth setup.
+     *
+     * @return boolean  Returns true if authentication is successful.
+     *                  Should send appropriate "not authorized" headers
+     *                  or other response codes/body if auth fails,
+     *                  and take care of exiting.
+     */
+    function authorize()
+    {
+        if (!$this->_requireAuthorization) {
+            return true;
+        }
+
+        $auth = &Auth::singleton($GLOBALS['conf']['auth']['driver']);
+
+        if (isset($_SERVER['PHP_AUTH_USER'])) {
+            $user = $_SERVER['PHP_AUTH_USER'];
+            $pass = $_SERVER['PHP_AUTH_PW'];
+        } elseif (isset($_SERVER['Authorization'])) {
+            $hash = str_replace('Basic ', '', $_SERVER['Authorization']);
+            $hash = base64_decode($hash);
+            if (strpos($hash, ':') !== false) {
+                list($user, $pass) = explode(':', $hash, 2);
+            }
+        }
+
+        if (!isset($user)
+            || !$auth->authenticate($user, array('password' => $pass))) {
+            if ($this->_requestMissingAuthorization) {
+                header('WWW-Authenticate: Basic realm="Horde RPC"');
+            }
+            header('HTTP/1.0 401 Unauthorized');
+            echo '401 Unauthorized';
+            exit;
+        }
+
+        return true;
+    }
+
+    /**
+     * Get the request body input. Different RPC backends can override
+     * this to return an open stream to php://stdin, for instance -
+     * whatever is easiest to handle in the getResponse() method.
+     *
+     * The base class implementation looks for $HTTP_RAW_POST_DATA and
+     * returns that if it's available; otherwise, it returns the
+     * contents of php://stdin.
+     *
+     * @return mixed  The input - a string (default), a filehandle, etc.
+     */
+    function getInput()
+    {
+        if (isset($GLOBALS['HTTP_RAW_POST_DATA'])) {
+            return $GLOBALS['HTTP_RAW_POST_DATA'];
+        } else {
+            return implode("\r\n", file('php://input'));
+        }
+    }
+
+    /**
+     * 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';
+    }
+
+    /**
+     * Returns the Content-Type of the response.
+     *
+     * @return string  The MIME Content-Type of the RPC response.
+     */
+    function getResponseContentType()
+    {
+        return 'text/xml';
+    }
+
+    /**
+     * 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',
+     *                          'xmlrpc' and 'jsonrpc' 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     A hash containing any necessary parameters for
+     *                          the method call.
+     * @param $options          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())
+    {
+        $driver = basename($driver);
+        $class = 'Horde_RPC_' . $driver;
+        if (!class_exists($class)) {
+            include 'Horde/RPC/' . $driver . '.php';
+        }
+
+        if (class_exists($class)) {
+            return call_user_func(array($class, 'request'), $url, $method, $params, $options);
+        } else {
+            include_once 'PEAR.php';
+            return PEAR::raiseError('Class definition of ' . $class . ' not found.');
+        }
+    }
+
+    /**
+     * Attempts to return a concrete RPC server instance based on
+     * $driver.
+     *
+     * @param mixed $driver  The type of concrete RPC subclass to return. If
+     *                       $driver is an array, then we will look in
+     *                       $driver[0]/lib/RPC/ for the subclass
+     *                       implementation named $driver[1].php.
+     * @param array $params  A hash containing any additional configuration or
+     *                       connection parameters a subclass might need.
+     *
+     * @return Horde_RPC  The newly created concrete Horde_RPC server instance,
+     *                    or PEAR_Error on error.
+     */
+    function factory($driver, $params = null)
+    {
+        $driver = basename($driver);
+        if ($driver == 'soap' && class_exists('SoapServer')) {
+            $driver = 'PhpSoap';
+        }
+
+        $class = 'Horde_RPC_' . $driver;
+        if (!class_exists($class)) {
+            include 'Horde/RPC/' . $driver . '.php';
+        }
+
+        if (class_exists($class)) {
+            return new $class($params);
+        } else {
+            include_once 'PEAR.php';
+            return PEAR::raiseError('Class definition of ' . $class . ' not found.');
+        }
+    }
+
+}
diff --git a/framework/RPC/RPC/PhpSoap.php b/framework/RPC/RPC/PhpSoap.php
new file mode 100644 (file)
index 0000000..c937936
--- /dev/null
@@ -0,0 +1,213 @@
+<?php
+/**
+ * The Horde_RPC_PhpSoap class provides a PHP 5 Soap implementation
+ * of the Horde RPC system.
+ *
+ * $Horde: framework/RPC/RPC/PhpSoap.php,v 1.2 2009/01/06 17:49:38 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.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  Chuck Hagenbuch <chuck@horde.org>
+ * @since   Horde 3.2
+ * @package Horde_RPC
+ */
+class Horde_RPC_PhpSoap extends Horde_RPC {
+
+    /**
+     * Resource handler for the RPC server.
+     *
+     * @var object
+     */
+    var $_server;
+
+    /**
+     * List of types to emit in the WSDL.
+     *
+     * @var array
+     */
+    var $_allowedTypes = array();
+
+    /**
+     * List of method names to allow.
+     *
+     * @var array
+     */
+    var $_allowedMethods = array();
+
+    /**
+     * Name of the SOAP service to use in the WSDL.
+     *
+     * @var string
+     */
+    var $_serviceName = null;
+
+    /**
+     * SOAP server constructor
+     *
+     * @access private
+     */
+    public function __construct($params = array())
+    {
+        parent::Horde_RPC($params);
+
+        if (!empty($params['allowedTypes'])) {
+            $this->_allowedTypes = $params['allowedTypes'];
+        }
+        if (!empty($params['allowedMethods'])) {
+            $this->_allowedMethods = $params['allowedMethods'];
+        }
+        if (!empty($params['serviceName'])) {
+            $this->_serviceName = $params['serviceName'];
+        }
+
+        $this->_server = new SoapServer(null, array('uri' => Horde::url($GLOBALS['registry']->get('webroot', 'horde') . '/rpc.php', true, false)));
+        $this->_server->addFunction(SOAP_FUNCTIONS_ALL);
+        $this->_server->setClass('Horde_RPC_PhpSoap_Caller', $params);
+    }
+
+    /**
+     * Takes an RPC request and returns the result.
+     *
+     * @param string  The raw request string.
+     *
+     * @return string  The XML encoded response from the server.
+     */
+    function getResponse($request)
+    {
+        if ($request == 'disco' || $request == 'wsdl') {
+            /* TODO - replace PEAR here? For now fall back to the PEAR
+             * server. */
+            if (!class_exists('Horde_RPC_soap')) {
+                include dirname(__FILE__) . '/soap.php';
+            }
+            $handler = new Horde_RPC_soap($this->_params);
+            return $handler->getResponse($request);
+        }
+
+        /* We can't use Util::bufferOutput() here for some reason. */
+        $beginTime = time();
+        ob_start();
+        $this->_server->handle($request);
+        Horde::logMessage(
+            sprintf('SOAP call: %s(%s) by %s serviced in %d seconds, sent %d bytes in response',
+                    $GLOBALS['__horde_rpc_PhpSoap']['lastMethodCalled'],
+                    implode(', ', array_map(create_function('$a', 'return is_array($a) ? "Array" : $a;'),
+                                            $GLOBALS['__horde_rpc_PhpSoap']['lastMethodParams'])),
+                    Auth::getAuth(),
+                    time() - $beginTime,
+                    ob_get_length()),
+            __FILE__, __LINE__, PEAR_LOG_INFO
+        );
+        return ob_get_clean();
+    }
+
+    /**
+     * Builds a SOAP request and sends it to the SOAP server.
+     *
+     * This statically called method is actually the SOAP client.
+     *
+     * @param string $url     The path to the SOAP server on the called host.
+     * @param string $method  The method to call.
+     * @param array $params   A hash containing any necessary parameters for
+     *                        the method call.
+     * @param $options  Optional associative array of parameters which can be:
+     *                  user                - Basic Auth username
+     *                  pass                - Basic Auth password
+     *                  proxy_host          - Proxy server host
+     *                  proxy_port          - Proxy server port
+     *                  proxy_user          - Proxy auth username
+     *                  proxy_pass          - Proxy auth password
+     *                  timeout             - Connection timeout in seconds.
+     *                  allowRedirects      - Whether to follow redirects or not
+     *                  maxRedirects        - Max number of redirects to follow
+     *                  namespace
+     *                  soapaction
+     *                  from                - SMTP, from address
+     *                  transfer-encoding   - SMTP, sets the
+     *                                        Content-Transfer-Encoding header
+     *                  subject             - SMTP, subject header
+     *                  headers             - SMTP, array-hash of extra smtp
+     *                                        headers
+     *
+     * @return mixed            The returned result from the method or a PEAR
+     *                          error object on failure.
+     */
+    public function request($url, $method, $params = null, $options = array())
+    {
+        if (!isset($options['timeout'])) {
+            $options['timeout'] = 5;
+        }
+        if (!isset($options['allowRedirects'])) {
+            $options['allowRedirects'] = true;
+            $options['maxRedirects']   = 3;
+        }
+        $options['location'] = $url;
+        $options['uri'] = $options['namespace'];
+
+        $soap = new SoapClient(null, $options);
+        return $soap->__soapCall($method, $params);
+    }
+
+}
+
+class Horde_RPC_PhpSoap_Caller {
+
+    /**
+     * List of method names to allow.
+     *
+     * @var array
+     */
+    protected $_allowedMethods = array();
+
+    /**
+     */
+    public function __construct($params = array())
+    {
+        if (!empty($params['allowedMethods'])) {
+            $this->_allowedMethods = $params['allowedMethods'];
+        }
+    }
+
+    /**
+     * Will be registered as the handler for all methods called in the
+     * SOAP server and will call the appropriate function through the registry.
+     *
+     * @todo  PEAR SOAP operates on a copy of this object at some unknown
+     *        point and therefore doesn't have access to instance
+     *        variables if they're set here. Instead, globals are used
+     *        to track the method name and args for the logging code.
+     *        Once this is PHP 5-only, the globals can go in favor of
+     *        instance variables.
+     *
+     * @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.
+     */
+    public function __call($method, $params)
+    {
+        $method = str_replace('.', '/', $method);
+
+        if (!empty($this->_params['allowedMethods']) &&
+            !in_array($method, $this->_params['allowedMethods'])) {
+            return sprintf(_("Method \"%s\" is not defined"), $method);
+        }
+
+        $GLOBALS['__horde_rpc_PhpSoap']['lastMethodCalled'] = $method;
+        $GLOBALS['__horde_rpc_PhpSoap']['lastMethodParams'] =
+            !empty($params) ? $params : array();
+
+        if (!$GLOBALS['registry']->hasMethod($method)) {
+            return sprintf(_("Method \"%s\" is not defined"), $method);
+        }
+
+        return $GLOBALS['registry']->call($method, $params);
+    }
+
+}
diff --git a/framework/RPC/RPC/jsonrpc.php b/framework/RPC/RPC/jsonrpc.php
new file mode 100644 (file)
index 0000000..2f2fbd3
--- /dev/null
@@ -0,0 +1,227 @@
+<?php
+/**
+ * $Horde: framework/RPC/RPC/jsonrpc.php,v 1.9 2009/02/21 01:58:14 chuck Exp $
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.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  Joey Hewitt <joey@joeyhewitt.com>
+ * @author  Jan Schneider <jan@horde.org>
+ * @since   Horde 3.2
+ * @package Horde_RPC
+ */
+
+/**
+ * The Horde_RPC_json-rpc class provides a JSON-RPC 1.1 implementation of the
+ * Horde RPC system.
+ *
+ * - Only POST requests are supported.
+ * - Named and positional parameters are accepted but the Horde registry only
+ *   works with positional parameters.
+ * - Service Descriptions are not supported yet.
+ *
+ * @link http://json-rpc.org
+ * @package Horde_RPC
+ */
+class Horde_RPC_jsonrpc extends Horde_RPC {
+
+    /**
+     * Returns the Content-Type of the response.
+     *
+     * @return string  The MIME Content-Type of the RPC response.
+     */
+    function getResponseContentType()
+    {
+        return 'application/json';
+    }
+
+    /**
+     * Sends an RPC request to the server and returns the result.
+     *
+     * @param string  The raw request string.
+     *
+     * @return string  The JSON encoded response from the server.
+     */
+    function getResponse($request)
+    {
+        $request = Horde_Serialize::unserialize($request, Horde_Serialize::JSON);
+
+        if (!is_object($request)) {
+            return $this->_raiseError('Request must be a JSON object', $request);
+        }
+
+        // Validate the request.
+        if (empty($request->method)) {
+            return $this->_raiseError('Request didn\'t specify a method name.', $request);
+        }
+
+        // Convert objects to associative arrays.
+        if (empty($request->params)) {
+            $params = array();
+        } else {
+            $params = $this->_objectsToArrays($request->params);
+            if (!is_array($params)) {
+                return $this->_raiseError('Parameters must be JSON objects or arrays.', $request);
+            }
+        }
+
+        // Check the method name.
+        $method = str_replace('.', '/', $request->method);
+        if (!$GLOBALS['registry']->hasMethod($method)) {
+            return $this->_raiseError('Method "' . $request->method . '" is not defined', $request);
+        }
+
+        // Call the method.
+        $result = $GLOBALS['registry']->call($method, $params);
+        if (is_a($result, 'PEAR_Error')) {
+            return $this->_raiseError($result, $request);
+        }
+
+        // Return result.
+        $response = array('version' => '1.1', 'result' => $result);
+        if (isset($request->id)) {
+            $response['id'] = $request->id;
+        }
+
+        return Horde_Serialize::serialize($response, Horde_Serialize::JSON);
+    }
+
+    /**
+     * Returns a specially crafted PEAR_Error object containing a JSON-RPC
+     * response in the error message.
+     *
+     * @param string|PEAR_Error $error  The error message or object.
+     * @param stdClass $request         The original request object.
+     *
+     * @return PEAR_Error  An error object suitable for a JSON-RPC 1.1
+     *                     conform error result.
+     */
+    function _raiseError($error, $request)
+    {
+        $code = $userinfo = null;
+        if (is_a($error, 'PEAR_Error')) {
+            $code = $error->getCode();
+            $userinfo = $error->getUserInfo();
+            $error = $error->getMessage();
+        }
+        $error = array('name' => 'JSONRPCError',
+                       'code' => $code ? $code : 999,
+                       'message' => $error);
+        if ($userinfo) {
+            $error['error'] = $userinfo;
+        }
+        $response = array('version' => '1.1', 'error' => $error);
+        if (isset($request->id)) {
+            $response['id'] = $request->id;
+        }
+
+        return PEAR::raiseError(Horde_Serialize::serialize($response, Horde_Serialize::JSON));
+    }
+
+    /**
+     * Builds an JSON-RPC request and sends it to the server.
+     *
+     * This statically called method is actually the JSON-RPC client.
+     *
+     * @param string $url     The path to the JSON-RPC server on the called
+     *                         host.
+     * @param string $method  The method to call.
+     * @param array $params   A hash containing any necessary parameters for
+     *                        the method call.
+     * @param $options        Optional associative array of parameters which
+     *                        can be:
+     *                        - user           - Basic Auth username
+     *                        - pass           - Basic Auth password
+     *                        - proxy_host     - Proxy server host
+     *                        - proxy_port     - Proxy server port
+     *                        - proxy_user     - Proxy auth username
+     *                        - proxy_pass     - Proxy auth password
+     *                        - timeout        - Connection timeout in seconds.
+     *                        - allowRedirects - Whether to follow redirects or
+     *                                           not
+     *                        - maxRedirects   - Max number of redirects to
+     *                                           follow
+     *
+     * @return mixed  The returned result from the method or a PEAR_Error on
+     *                failure.
+     */
+    function request($url, $method, $params = null, $options = array())
+    {
+        $options['method'] = 'POST';
+        $language = isset($GLOBALS['language']) ? $GLOBALS['language'] :
+                    (isset($_SERVER['LANG']) ? $_SERVER['LANG'] : '');
+
+        if (!isset($options['timeout'])) {
+            $options['timeout'] = 5;
+        }
+        if (!isset($options['allowRedirects'])) {
+            $options['allowRedirects'] = true;
+            $options['maxRedirects'] = 3;
+        }
+        if (!isset($options['proxy_host']) &&
+            !empty($GLOBALS['conf']['http']['proxy']['proxy_host'])) {
+            $options = array_merge($options, $GLOBALS['conf']['http']['proxy']);
+        }
+
+        require_once 'HTTP/Request.php';
+        $http = new HTTP_Request($url, $options);
+        if (!empty($language)) {
+            $http->addHeader('Accept-Language', $language);
+        }
+        $http->addHeader('User-Agent', 'Horde RPC client');
+        $http->addHeader('Accept', 'application/json');
+        $http->addHeader('Content-Type', 'application/json');
+
+        $data = array('version' => '1.1', 'method' => $method);
+        if (!empty($params)) {
+            $data['params'] = $params;
+        }
+        $http->addRawPostData(Horde_Serialize::serialize($data, Horde_Serialize::JSON));
+
+        $result = $http->sendRequest();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        } elseif ($http->getResponseCode() == 500) {
+            $response = Horde_Serialize::unserialize($http->getResponseBody(), Horde_Serialize::JSON);
+            if (is_a($response, 'stdClass') &&
+                isset($response->error) &&
+                is_a($response->error, 'stdClass') &&
+                isset($response->error->name) &&
+                $response->error->name == 'JSONRPCError') {
+                return PEAR::raiseError($response->error->message,
+                                        $response->error->code,
+                                        null, null,
+                                        isset($response->error->error) ? $response->error->error : null);
+            }
+            return PEAR::raiseError($http->getResponseBody());
+        } elseif ($http->getResponseCode() != 200) {
+            return PEAR::raiseError('Request couldn\'t be answered. Returned errorcode: "' . $http->getResponseCode(), 'horde.error');
+        }
+
+        return Horde_Serialize::unserialize($http->getResponseBody(), Horde_Serialize::JSON);
+    }
+
+    /**
+     * Converts stdClass object to associative arrays.
+     *
+     * @param $data mixed  Any stdClass object, array, or scalar.
+     *
+     * @return mixed  stdClass objects are returned as asscociative arrays,
+     *                scalars as-is, and arrays with their elements converted.
+     */
+    function _objectsToArrays($data)
+    {
+        if (is_a($data, 'stdClass')) {
+            $data = get_object_vars($data);
+        }
+        if (is_array($data)) {
+            foreach ($data as $key => $value) {
+                $data[$key] = $this->_objectsToArrays($value);
+            }
+        }
+        return $data;
+    }
+
+}
diff --git a/framework/RPC/RPC/phpgw.php b/framework/RPC/RPC/phpgw.php
new file mode 100644 (file)
index 0000000..7bee4be
--- /dev/null
@@ -0,0 +1,183 @@
+<?php
+/**
+ * The Horde_RPC_phpgw class provides an XMLRPC implementation of the
+ * Horde RPC system compatible with phpgw. It is based on the
+ * xmlrpc.php implementation by Jan Schneider.
+ *
+ * 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  Michael Braun <mi.braun@onlinehome.de>
+ * @since   Horde 3.2
+ * @package Horde_RPC
+ */
+class Horde_RPC_phpgw extends Horde_RPC {
+
+    /**
+     * Resource handler for the XML-RPC server.
+     *
+     * @var resource
+     */
+    var $_server;
+
+    /**
+     * XMLRPC server constructor.
+     */
+    function Horde_RPC_phpgw()
+    {
+        parent::Horde_RPC();
+
+        $this->_server = xmlrpc_server_create();
+
+        // Register only phpgw services.
+        foreach ($GLOBALS['registry']->listMethods('phpgw') as $method) {
+            $methods = explode('/', $method);
+            array_shift($methods);
+            $method = implode('.', $methods);
+            xmlrpc_server_register_method($this->_server, $method, array('Horde_RPC_phpgw', '_dispatcher'));
+        }
+    }
+
+    /**
+     * Authorization is done by xmlrpc method system.login.
+     */
+    function authorize()
+    {
+        return true;
+    }
+
+    /**
+     * 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)
+    {
+        $response = null;
+        return xmlrpc_server_call_method($this->_server, $request, $response);
+    }
+
+    /**
+     * 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('.', '/', 'phpgw.' . $method);
+
+        if (!$registry->hasMethod($method)) {
+            Horde::logMessage(sprintf(_("Method \"%s\" is not defined"), $method), __FILE__, __LINE__, PEAR_LOG_NOTICE);
+            return sprintf(_("Method \"%s\" is not defined"), $method);
+        }
+
+        // Try to resume a session
+        if (isset($params[0]['kp3']) && $params[0]["kp3"] == session_name() && session_id() != $params[0]["sessionid"]) {
+            Horde::logMessage("manually reload session ".$params[0]["sessionid"], __FILE__, __LINE__, PEAR_LOG_NOTICE);
+            // Make sure to force a completely new session ID and clear
+            // all session data.
+            if (version_compare(PHP_VERSION, '4.3.3') !== -1) {
+                session_regenerate_id();
+                session_unset();
+                session_id($params[0]["sessionid"]);
+            } else {
+                @session_destroy();
+                session_id($params[0]["sessionid"]);
+                Horde::setupSessionHandler();
+                session_start();
+            }
+        }
+
+        // Be authenticated or call system.login.
+        $auth = &Auth::singleton($GLOBALS['conf']['auth']['driver']);
+        $authenticated = $auth->isAuthenticated() || $method== "phpgw/system/login";
+
+        if ($authenticated) {
+            Horde::logMessage("rpc call $method allowed", __FILE__, __LINE__, PEAR_LOG_NOTICE);
+            return $registry->call($method, $params);
+        } else {
+            return PEAR::raiseError(_("You did not authenticate."), 'horde.error');
+            // return parent::authorize();
+            // error 9 "access denied"
+        }
+    }
+
+    /**
+     * Builds an XMLRPC request and sends it to the XMLRPC server.
+     *
+     * This statically called method is actually the XMLRPC client.
+     *
+     * @param string $url     The path to the XMLRPC server on the called host.
+     * @param string $method  The method to call.
+     * @param array $params   A hash containing any necessary parameters for
+     *                        the method call.
+     * @param $options  Optional associative array of parameters which can be:
+     *                  user           - Basic Auth username
+     *                  pass           - Basic Auth password
+     *                  proxy_host     - Proxy server host
+     *                  proxy_port     - Proxy server port
+     *                  proxy_user     - Proxy auth username
+     *                  proxy_pass     - Proxy auth password
+     *                  timeout        - Connection timeout in seconds.
+     *                  allowRedirects - Whether to follow redirects or not
+     *                  maxRedirects   - Max number of redirects to follow
+     *
+     * @return mixed            The returned result from the method or a PEAR
+     *                          error object on failure.
+     */
+    function request($url, $method, $params = null, $options = array())
+    {
+        $options['method'] = 'POST';
+        $language = isset($GLOBALS['language']) ? $GLOBALS['language'] :
+            (isset($_SERVER['LANG']) ? $_SERVER['LANG'] : '');
+
+        if (!isset($options['timeout'])) {
+            $options['timeout'] = 5;
+        }
+        if (!isset($options['allowRedirects'])) {
+            $options['allowRedirects'] = true;
+            $options['maxRedirects'] = 3;
+        }
+        if (!isset($options['proxy_host']) && !empty($GLOBALS['conf']['http']['proxy']['proxy_host'])) {
+            $options = array_merge($options, $GLOBALS['conf']['http']['proxy']);
+        }
+
+        require_once 'HTTP/Request.php';
+        $http = new HTTP_Request($url, $options);
+        if (!empty($language)) {
+            $http->addHeader('Accept-Language', $language);
+        }
+        $http->addHeader('User-Agent', 'Horde RPC client');
+        $http->addHeader('Content-Type', 'text/xml');
+        $http->addRawPostData(xmlrpc_encode_request($method, $params));
+
+        $result = $http->sendRequest();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        } elseif ($http->getResponseCode() != 200) {
+            return PEAR::raiseError(_("Request couldn't be answered. Returned errorcode: ") . $http->getResponseCode(), 'horde.error');
+        } elseif (strpos($http->getResponseBody(), '<?xml') === false) {
+            return PEAR::raiseError(_("No valid XML data returned"), 'horde.error', null, null, $http->getResponseBody());
+        } else {
+            $response = @xmlrpc_decode(substr($http->getResponseBody(), strpos($http->getResponseBody(), '<?xml')));
+            if (is_array($response) && isset($response['faultString'])) {
+                return PEAR::raiseError($response['faultString'], 'horde.error');
+            } elseif (is_array($response) && isset($response[0]) &&
+                      is_array($response[0]) && isset($response[0]['faultString'])) {
+                return PEAR::raiseError($response[0]['faultString'], 'horde.error');
+            }
+            return $response;
+        }
+    }
+
+}
diff --git a/framework/RPC/RPC/soap.php b/framework/RPC/RPC/soap.php
new file mode 100644 (file)
index 0000000..182677c
--- /dev/null
@@ -0,0 +1,267 @@
+<?php
+/**
+ * The Horde_RPC_soap class provides an SOAP implementation of the
+ * Horde RPC system.
+ *
+ * $Horde: framework/RPC/RPC/soap.php,v 1.30 2009/01/06 17:49:38 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.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>
+ * @since   Horde 3.0
+ * @package Horde_RPC
+ */
+class Horde_RPC_soap extends Horde_RPC {
+
+    /**
+     * Resource handler for the RPC server.
+     *
+     * @var object
+     */
+    var $_server;
+
+    /**
+     * List of types to emit in the WSDL.
+     *
+     * @var array
+     */
+    var $_allowedTypes = array();
+
+    /**
+     * List of method names to allow.
+     *
+     * @var array
+     */
+    var $_allowedMethods = array();
+
+    /**
+     * Name of the SOAP service to use in the WSDL.
+     *
+     * @var string
+     */
+    var $_serviceName = null;
+
+    /**
+     * Hash holding all methods' signatures.
+     *
+     * @var array
+     */
+    var $__dispatch_map = array();
+
+    /**
+     * SOAP server constructor
+     *
+     * @access private
+     */
+    function Horde_RPC_soap($params = null)
+    {
+        parent::Horde_RPC($params);
+
+        if (!empty($params['allowedTypes'])) {
+            $this->_allowedTypes = $params['allowedTypes'];
+        }
+        if (!empty($params['allowedMethods'])) {
+            $this->_allowedMethods = $params['allowedMethods'];
+        }
+        if (!empty($params['serviceName'])) {
+            $this->_serviceName = $params['serviceName'];
+        }
+
+        require_once 'SOAP/Server.php';
+        $this->_server = new SOAP_Server();
+        $this->_server->_auto_translation = true;
+    }
+
+    /**
+     * Fills a hash that is used by the SOAP server with the signatures of
+     * all available methods.
+     */
+    function _setupDispatchMap()
+    {
+        global $registry;
+
+        $methods = $registry->listMethods();
+        foreach ($methods as $method) {
+            $signature = $registry->getSignature($method);
+            if (!is_array($signature)) {
+                continue;
+            }
+            if (!empty($this->_allowedMethods) &&
+                !in_array($method, $this->_allowedMethods)) {
+                continue;
+            }
+            $method = str_replace('/', '.', $method);
+            $this->__dispatch_map[$method] = array(
+                'in' => $signature[0],
+                'out' => array('output' => $signature[1])
+            );
+        }
+
+        $this->__typedef = array();
+        foreach ($registry->listTypes() as $type => $params) {
+            if (!empty($this->_allowedTypes) &&
+                !in_array($type, $this->_allowedTypes)) {
+                continue;
+            }
+
+            $this->__typedef[$type] = $params;
+        }
+    }
+
+    /**
+     * Returns the signature of a method.
+     * Internally used by the SOAP server.
+     *
+     * @param string $method  A method name.
+     *
+     * @return array  An array describing the method's signature.
+     */
+    function __dispatch($method)
+    {
+        global $registry;
+        $method = str_replace('.', '/', $method);
+
+        $signature = $registry->getSignature($method);
+        if (!is_array($signature)) {
+            return null;
+        }
+
+        return array('in' => $signature[0],
+                     'out' => array('output' => $signature[1]));
+    }
+
+    /**
+     * Will be registered as the handler for all methods called in the
+     * SOAP server and will call the appropriate function through the registry.
+     *
+     * @todo  PEAR SOAP operates on a copy of this object at some unknown
+     *        point and therefore doesn't have access to instance
+     *        variables if they're set here. Instead, globals are used
+     *        to track the method name and args for the logging code.
+     *        Once this is PHP 5-only, the globals can go in favor of
+     *        instance variables.
+     *
+     * @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)
+    {
+        global $registry;
+        $method = str_replace('.', '/', $method);
+
+        if (!empty($this->_params['allowedMethods']) &&
+            !in_array($method, $this->_params['allowedMethods'])) {
+            return sprintf(_("Method \"%s\" is not defined"), $method);
+        }
+
+        $GLOBALS['__horde_rpc_soap']['lastMethodCalled'] = $method;
+        $GLOBALS['__horde_rpc_soap']['lastMethodParams'] =
+            !empty($params) ? $params : array();
+
+        if (!$registry->hasMethod($method)) {
+            return sprintf(_("Method \"%s\" is not defined"), $method);
+        }
+
+        $this->_server->bindWSDL(Horde::url($registry->get('webroot', 'horde') . '/rpc.php?wsdl', true, false));
+        return $registry->call($method, $params);
+    }
+
+    /**
+     * Takes an RPC request and returns the result.
+     *
+     * @param string  The raw request string.
+     *
+     * @return string  The XML encoded response from the server.
+     */
+    function getResponse($request)
+    {
+        $this->_server->addObjectMap($this, 'urn:horde');
+
+        if ($request == 'disco' || $request == 'wsdl') {
+            require_once 'SOAP/Disco.php';
+            $disco = new SOAP_DISCO_Server($this->_server,
+                !empty($this->_serviceName) ? $this->_serviceName : 'horde');
+            if ($request == 'wsdl') {
+                $this->_setupDispatchMap();
+                return $disco->getWSDL();
+            } else {
+                return $disco->getDISCO();
+            }
+        }
+
+        $this->_server->setCallHandler(array($this, '_dispatcher'));
+
+        /* We can't use Util::bufferOutput() here for some reason. */
+        $beginTime = time();
+        ob_start();
+        $this->_server->service($request);
+        Horde::logMessage(
+            sprintf('SOAP call: %s(%s) by %s serviced in %d seconds, sent %d bytes in response',
+                    $GLOBALS['__horde_rpc_soap']['lastMethodCalled'],
+                    is_array($GLOBALS['__horde_rpc_soap']['lastMethodParams'])
+                        ? implode(', ', array_map(create_function('$a', 'return is_array($a) ? "Array" : $a;'),
+                                                  $GLOBALS['__horde_rpc_soap']['lastMethodParams']))
+                        : '',
+                    Auth::getAuth(),
+                    time() - $beginTime,
+                    ob_get_length()),
+            __FILE__, __LINE__, PEAR_LOG_INFO
+        );
+        return ob_get_clean();
+    }
+
+    /**
+     * Builds an SOAP request and sends it to the SOAP server.
+     *
+     * This statically called method is actually the SOAP client.
+     *
+     * @param string $url     The path to the SOAP server on the called host.
+     * @param string $method  The method to call.
+     * @param array $params   A hash containing any necessary parameters for
+     *                        the method call.
+     * @param $options  Optional associative array of parameters which can be:
+     *                  user                - Basic Auth username
+     *                  pass                - Basic Auth password
+     *                  proxy_host          - Proxy server host
+     *                  proxy_port          - Proxy server port
+     *                  proxy_user          - Proxy auth username
+     *                  proxy_pass          - Proxy auth password
+     *                  timeout             - Connection timeout in seconds.
+     *                  allowRedirects      - Whether to follow redirects or not
+     *                  maxRedirects        - Max number of redirects to follow
+     *                  namespace
+     *                  soapaction
+     *                  from                - SMTP, from address
+     *                  transfer-encoding   - SMTP, sets the
+     *                                        Content-Transfer-Encoding header
+     *                  subject             - SMTP, subject header
+     *                  headers             - SMTP, array-hash of extra smtp
+     *                                        headers
+     *
+     * @return mixed            The returned result from the method or a PEAR
+     *                          error object on failure.
+     */
+    function request($url, $method, $params = null, $options = array())
+    {
+        if (!isset($options['timeout'])) {
+            $options['timeout'] = 5;
+        }
+        if (!isset($options['allowRedirects'])) {
+            $options['allowRedirects'] = true;
+            $options['maxRedirects']   = 3;
+        }
+
+        require_once 'SOAP/Client.php';
+        $soap = new SOAP_Client($url, false, false, $options);
+        return $soap->call($method, $params, $options['namespace']);
+    }
+
+}
diff --git a/framework/RPC/RPC/syncml.php b/framework/RPC/RPC/syncml.php
new file mode 100644 (file)
index 0000000..87494ae
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+
+require_once 'SyncML.php';
+require_once 'SyncML/Backend.php';
+
+/**
+ * The Horde_RPC_syncml class provides a SyncML implementation of the Horde
+ * RPC system.
+ *
+ * $Horde: framework/RPC/RPC/syncml.php,v 1.45 2009/01/06 17:49:38 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.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  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Anthony Mills <amills@pyramid6.com>
+ * @since   Horde 3.0
+ * @package Horde_RPC
+ */
+
+class Horde_RPC_syncml extends Horde_RPC {
+
+    /**
+     * SyncML handles authentication internally, so bypass the RPC framework
+     * auth check by just returning true here.
+     */
+    function authorize()
+    {
+        return true;
+    }
+
+    /**
+     * Sends an RPC request to the server and returns the result.
+     *
+     * @param string $request  The raw request string.
+     *
+     * @return string  The XML encoded response from the server.
+     */
+    function getResponse($request)
+    {
+        $backendparms = array(
+            /* Write debug output to this dir, must be writeable be web
+             * server. */
+            'debug_dir' => '/tmp/sync',
+            /* Log all (wb)xml packets received or sent to debug_dir. */
+            'debug_files' => true,
+            /* Log everything. */
+            'log_level' => PEAR_LOG_DEBUG);
+
+        /* Create the backend. */
+        $GLOBALS['backend'] = SyncML_Backend::factory('Horde', $backendparms);
+
+        /* Handle request. */
+        $h = new SyncML_ContentHandler();
+        $response = $h->process(
+            $request, $this->getResponseContentType(),
+            Horde::url($GLOBALS['registry']->get('webroot', 'horde') . '/rpc.php',
+                       true, -1));
+
+        /* Close the backend. */
+        $GLOBALS['backend']->close();
+
+        return $response;
+    }
+
+    /**
+     * Returns the Content-Type of the response.
+     *
+     * @return string  The MIME Content-Type of the RPC response.
+     */
+    function getResponseContentType()
+    {
+        return 'application/vnd.syncml+xml';
+    }
+
+}
diff --git a/framework/RPC/RPC/syncml_wbxml.php b/framework/RPC/RPC/syncml_wbxml.php
new file mode 100644 (file)
index 0000000..28b6203
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+require_once dirname(__FILE__) . '/syncml.php';
+
+/**
+ * The Horde_RPC_syncml_wbxml class provides a SyncML implementation of the
+ * Horde RPC system using WBXML encoding.
+ *
+ * $Horde: framework/RPC/RPC/syncml_wbxml.php,v 1.29 2009/01/06 17:49:38 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.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  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Anthony Mills <amills@pyramid6.com>
+ * @since   Horde 3.0
+ * @package Horde_RPC
+ */
+class Horde_RPC_syncml_wbxml extends Horde_RPC_syncml {
+
+    /**
+     * Returns the Content-Type of the response.
+     *
+     * @return string  The MIME Content-Type of the RPC response.
+     */
+    function getResponseContentType()
+    {
+        return 'application/vnd.syncml+wbxml';
+    }
+
+}
diff --git a/framework/RPC/RPC/webdav.php b/framework/RPC/RPC/webdav.php
new file mode 100644 (file)
index 0000000..f246b10
--- /dev/null
@@ -0,0 +1,3332 @@
+<?php
+/**
+ * The Horde_RPC_webdav class provides a WebDAV implementation of the
+ * Horde RPC system.
+ *
+ * $Horde: framework/RPC/RPC/webdav.php,v 1.49 2009/03/05 04:20:33 slusarz Exp $
+ *
+ * Copyright 2008-2009 The Horde Project (http://www.horde.org/)
+ *
+ * Derived from the HTTP_WebDAV_Server PEAR package:
+ * +------------------------------------------------------------------------+
+ * | Portions Copyright 2002-2007 Christian Stocker, Hartmut Holzgraefe |
+ * | All rights reserved                                                    |
+ * |                                                                        |
+ * | Redistribution and use in source and binary forms, with or without     |
+ * | modification, are permitted provided that the following conditions     |
+ * | are met:                                                               |
+ * |                                                                        |
+ * | 1. Redistributions of source code must retain the above copyright      |
+ * |    notice, this list of conditions and the following disclaimer.       |
+ * | 2. Redistributions in binary form must reproduce the above copyright   |
+ * |    notice, this list of conditions and the following disclaimer in     |
+ * |    the documentation and/or other materials provided with the          |
+ * |    distribution.                                                       |
+ * | 3. The names of the authors may not be used to endorse or promote      |
+ * |    products derived from this software without specific prior          |
+ * |    written permission.                                                 |
+ * |                                                                        |
+ * | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS    |
+ * | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT      |
+ * | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS      |
+ * | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE         |
+ * | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,    |
+ * | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,   |
+ * | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;       |
+ * | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER       |
+ * | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT     |
+ * | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN      |
+ * | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE        |
+ * | POSSIBILITY OF SUCH DAMAGE.                                            |
+ * +------------------------------------------------------------------------+
+ *
+ * Portions Copyright 2004-2009 The Horde Project (http://www.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  Chuck Hagenbuch <chuck@horde.org>
+ * @author  Ben Klang <ben@alkaloid.net>
+ * @author  Hartmut Holzgraefe
+ * @author  Christian Stocker
+ * @since   Horde 3.0
+ * @package Horde_RPC
+ */
+
+// Use Horde's Xml_Element to construct the DAV responses
+require_once 'Horde/Xml/Element.php';
+
+class Horde_RPC_webdav extends Horde_RPC {
+
+    /**
+     * CalDAV XML namespace
+     *
+     * @var string
+     */
+    const CALDAVNS = 'urn:ietf:params:xml:ns:caldav';
+
+    /**
+     * Realm string to be used in authentification popups
+     *
+     * @var string
+     */
+    var $http_auth_realm = 'Horde WebDAV';
+
+    /**
+     * String to be used in "X-Dav-Powered-By" header
+     *
+     * @var string
+     */
+    var $dav_powered_by = 'Horde WebDAV Server';
+
+    /**
+     * success state flag
+     *
+     * @var bool
+     * @access public
+     */
+    var $parseSuccess = false;
+
+    /**
+     * found properties are collected here
+     *
+     * @var array
+     * @access public
+     */
+    var $parseProps = false;
+
+    /**
+     * internal tag nesting depth counter
+     *
+     * @var int
+     * @access private
+     */
+    var $parseDepth = 0;
+
+    /**
+     * lock type, currently only "write"
+     *
+     * @var string
+     * @access public
+     */
+    var $locktype = "";
+
+    /**
+     * lock scope, "shared" or "exclusive"
+     *
+     * @var string
+     * @access public
+     */
+    var $lockscope = "";
+
+    /**
+     * lock owner information
+     *
+     * @var string
+     * @access public
+     */
+    var $owner = "";
+
+    /**
+     * flag that is set during lock owner read
+     *
+     * @var bool
+     * @access private
+     */
+    var $collect_owner = false;
+
+    /**
+     *
+     * 
+     * @var
+     * @access
+     */
+    var $mode;
+
+    /**
+     *
+     * 
+     * @var
+     * @access
+     */
+    var $current;
+
+    /**
+     * complete URI for this request
+     *
+     * @var string 
+     */
+    var $uri;
+    
+    
+    /**
+     * base URI for this request
+     *
+     * @var string 
+     */
+    var $base_uri;
+
+
+    /**
+     * URI path for this request
+     *
+     * @var string 
+     */
+    var $path;
+
+    /**
+     * Remember parsed If: (RFC2518/9.4) header conditions  
+     *
+     * @var array
+     */
+    var $_if_header_uris = array();
+
+    /**
+     * HTTP response status/message
+     *
+     * @var string
+     */
+    var $_http_status = "200 OK";
+
+    /**
+     * encoding of property values passed in
+     *
+     * @var string
+     */
+    var $_prop_encoding = "utf-8";
+
+    /**
+     * Copy of $_SERVER superglobal array
+     *
+     * Derived classes may extend the constructor to
+     * modify its contents
+     *
+     * @var array
+     */
+    var $_SERVER;
+
+    /**
+     * Mapping of XML namespaces to their XML nickname
+     *
+     * @var array
+     */
+    var $ns_hash = array('DAV:' => 'D');
+
+    /**
+     * Xml_Element object
+     * @var object
+     */
+    var $_xml;
+
+    /**
+     * WebDav server constructor.
+     *
+     * @access private
+     */
+    function Horde_RPC_webdav()
+    {
+        // PHP messages destroy XML output -> switch them off
+        ini_set("display_errors", 0);
+
+        // copy $_SERVER variables to local _SERVER array
+        // so that derived classes can simply modify these
+        $this->_SERVER = $_SERVER;
+
+        parent::Horde_RPC();
+    }
+
+    /**
+     * WebDAV handles authentication internally, so bypass the
+     * system-level auth check by just returning true here.
+     */
+    function authorize()
+    {
+        return true;
+    }
+
+    /**
+     * If the webdav backend is used, the input should not be read, it is
+     * being read by HTTP_WebDAV_Server.
+     */
+    function getInput()
+    {
+    }
+
+    /**
+     * 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)
+    {
+        $this->ServeRequest();
+        exit;
+    }
+
+
+    /**
+     * GET implementation.
+     *
+     * @param array $options  Array of input and output parameters.
+     * <br><strong>input</strong><ul>
+     * <li> path -
+     * </ul>
+     * <br><strong>output</strong><ul>
+     * <li> size -
+     * </ul>
+     *
+     * @return string|boolean  HTTP-Statuscode.
+     */
+    function GET(&$options)
+    {
+        if ($options['path'] == '/') {
+            $options['mimetype'] = 'httpd/unix-directory';
+        } else {
+            // Ensure we only retrieve the exact item
+            $options['depth'] = 0;
+            $result = $this->_list($options);
+            if (is_a($result, 'PEAR_Error') && $result->getCode()) {
+                // Allow called applications to set the result code
+                return $this->_checkHTTPCode($result->getCode())
+                    . ' ' . $result->getMessage();
+            } elseif ($result === false) {
+                return '404 File Not Found';
+            }
+            $options = $result;
+        }
+
+        return true;
+    }
+
+    /**
+     * PUT implementation.
+     *
+     * @param array &$options  Parameter passing array.
+     *
+     * @return string|boolean  HTTP-Statuscode.
+     */
+    function PUT(&$options)
+    {
+        $path = trim($options['path'], '/');
+
+        if (empty($path)) {
+            return '403 PUT requires a path.';
+        }
+
+        $pieces = explode('/', $path);
+
+        if (count($pieces) < 2 || empty($pieces[0])) {
+            return '403 PUT denied outside of application directories.';
+        }
+
+        $content = '';
+        while (!feof($options['stream'])) {
+            $content .= fgets($options['stream']);
+        }
+
+        $result = $GLOBALS['registry']->callByPackage($pieces[0], 'put', array('path' => $path, 'content' => $content, 'type' => $options['content_type']));
+        if (is_a($result, 'PEAR_Error')) {
+            Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+            if ($result->getCode()) {
+                return $this->_checkHTTPCode($result->getCode())
+                    . ' ' . $result->getMessage();
+            } else {
+                return '500 Internal Server Error. Check server logs';
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Performs a WebDAV DELETE.
+     *
+     * Deletes a single object from a database. The path passed in must
+     * be in [app]/[path] format.
+     *
+     * @see HTTP_WebDAV_Server::http_DELETE()
+     *
+     * @param array $options An array of parameters from the setup
+     * method in HTTP_WebDAV_Server.
+     *
+     * @return string|boolean  HTTP-Statuscode.
+     */
+    function DELETE($options)
+    {
+        $path = $options['path'];
+        $pieces = explode('/', trim($this->path, '/'), 2);
+
+        if (count($pieces) == 2) {
+            $app = $pieces[0];
+            $path = $pieces[1];
+
+            // TODO: Support HTTP/1.1 If-Match on ETag here
+
+            // Delete access is checked in each app.
+            $result = $GLOBALS['registry']->callByPackage($app, 'path_delete', array($path));
+            if (is_a($result, 'PEAR_Error')) {
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_INFO);
+                if ($result->getCode()) {
+                    return $this->_checkHTTPCode($result->getCode())
+                        . ' ' . $result->getMessage();
+                } else {
+                    return '500 Internal Server Error. Check server logs';
+                }
+            }
+            return '204 No Content';
+        } else {
+            Horde::logMessage(sprintf(_("Error deleting from path %s; must be [app]/[path]", $options['path'])), __FILE__, __LINE__, PEAR_LOG_INFO);
+            return '403 Must supply a resource within the application to delete.';
+        }
+    }
+
+    /**
+     * PROPFIND method handler
+     *
+     * @param array $options  General parameter passing array.
+     * @param array &$files   Return array for file properties.
+     *
+     * @return boolean  True on success.
+     */
+    function PROPFIND($options, &$files)
+    {
+        $list = $this->_list($options);
+        if ($list === false || is_a($list, 'PEAR_Error')) {
+            // Always return '404 File Not Found';
+            // Work around HTTP_WebDAV_Server behavior.
+            // See: http://pear.php.net/bugs/bug.php?id=11390
+            return false;
+        }
+        $files['files'] = $list;
+        return true;
+    }
+
+    /**
+     * MKCOL method handler
+     *
+     * @param array $options
+     * @return string HTTP response string
+     */
+    function MKCOL($options)
+    {
+        $path = $options['path'];
+        if (substr($path, 0, 1) == '/') {
+            $path = substr($path, 1);
+        }
+
+        // Take the module name from the path
+        $pieces = explode('/', $path, 2);
+        if (count($pieces) == 2) {
+            // Send the request to the application
+            $result = $GLOBALS['registry']->callByPackage($pieces[0], 'mkcol', array('path' => $path));
+            if (is_a($result, 'PEAR_Error')) {
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                if ($result->getCode()) {
+                    return $this->_checkHTTPCode($result->getCode())
+                        . ' ' . $result->getMessage();
+                } else {
+                    return '500 Internal Server Error. Check server logs';
+                }
+            }
+        } else {
+            Horde::logMessage(sprintf(_("Unable to create directory %s; must be [app]/[path]"), $path), __FILE__, __LINE__, PEAR_LOG_INFO);
+            return '403 Must specify a resource within an application.  MKCOL disallowed at top level.';
+        }
+
+        return '200 OK';
+    }
+
+    /**
+     * MOVE method handler
+     *
+     * @param array $options
+     * @return string HTTP response string
+     */
+    function MOVE($options)
+    {
+        $path = $options['path'];
+        if (substr($path, 0, 1) == '/') {
+            $path = substr($path, 1);
+        }
+
+        // Take the module name from the path
+        $sourcePieces = explode('/', $path, 2);
+        if (count($sourcePieces) == 2) {
+            $destPieces = explode('/', $options['dest'], 2);
+            if (!(count($destPieces) == 2) || $sourcesPieces[0] != $destPieces[0]) {
+                return '400 Can not move across applications.';
+            }
+            // Send the request to the module
+            $result = $GLOBALS['registry']->callByPackage($sourcePieces[0], 'move', array('path' => $path, 'dest' => $options['dest']));
+            if (is_a($result, 'PEAR_Error')) {
+                Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR);
+                if ($result->getCode()) {
+                    return $this->_checkHTTPCode($result->getCode())
+                        . ' ' . $result->getMessage();
+                } else {
+                    return '500 Internal Server Error. Check server logs';
+                }
+            }
+        } else {
+            Horde::logMessage(sprintf(_("Unable to rename %s; must be [app]/[path] and within the same application."), $path), __FILE__, __LINE__, PEAR_LOG_INFO);
+            return '403 Must specify a resource within an application.  MOVE disallowed at top level.';
+        }
+
+        return '200 OK';
+    }
+
+    /**
+     * Generates a response to a GET or PROPFIND request.
+     *
+     * @param array $options     Array of WebDAV options
+     *
+     * @return mixed  Array of objects with properties if the request is a dir,
+     *                array of file metadata + data if request is a file,
+     *                false if the object is not found.
+     */
+    function _list($options)
+    {
+        global $registry;
+
+        // $path (or $options['path']) is the node on which we will list 
+        // collections and resources.  $this->path is the path of the original
+        // request from the client.
+        $path = $options['path'];
+        $depth = $options['depth'];
+
+        // Collect the requested properties
+        if (!isset($options['props']) || empty($options['props'])) {
+            Horde::logMessage(('Invalid or missing properties requested by WebDAV client.  Using a default list of properties.'), __FILE__, __LINE__, PEAR_LOG_INFO);
+            $properties = array('name', 'browseable', 'contenttype', 'contentlength', 'created', 'modified');
+        } else {
+            // Construct an array of properties including the XML namespace
+            // if not part of the basic DAV namespace.
+            $properties = array();
+            foreach ($options['props'] as $prop) {
+                if ($prop['xmlns'] == 'DAV:') {
+                    $properties[] = $prop['name'];
+                } else {
+                    $properties[] = $prop['xmlns'] . ':' . $prop['name'];
+                }
+            }
+        }
+
+        // $list will contain the data to return to the client
+        $list = array();
+        
+        if ($path == '/') {
+            // $root is a virtual collection describing the root of the Horde
+            // WebDAV space
+            $now = time();
+            $root = array('name' => '/',
+                          'created' => $now,
+                          'mtime' => $now,
+                          'mimetype' => 'httpd/unix-directory',
+                          'contentlength' => 0,
+                          'resourcetype' => 'collection');
+            $list[] = array('path' => $path,
+                            'props' => $this->_getProps($options['props'], $root));
+
+            // See if we should continue traversing down the tree
+            if ($depth == 0) { return $list; }
+
+            $apps = $registry->listApps(null, false, PERMS_READ);
+            if (is_a($apps, 'PEAR_Error')) {
+                Horde::logMessage($apps, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $apps;
+            }
+            foreach ($apps as $app) {
+                // Call each application's browse method to get
+                // their collections
+                if ($registry->hasMethod('browse', $app)) {
+                    $origdepth = $options['depth'];
+                    if ($options['depth'] == 1) {
+                        // Make sure the applications each only return one level
+                        $options['depth'] = 0;
+                    }
+                    $results = $registry->callByPackage($app, 'browse', array('path' => '/', 'depth' => $options['depth']));
+                    $options['depth'] = $origdepth;
+
+                    foreach ($results as $itemPath => $item) {
+                        if (($item !== false) && !is_a($item, 'PEAR_Error')) {
+                            // A false return is "file not found"
+                            // A PEAR_Error return is an error.  Silently ignore
+                            // those errors from the applications.  Errors will
+                            // will nevertheless be logged.
+                            $list[] =
+                                array('path' => $this->path . '/' . $itemPath,
+                                      'props' => $this->_getProps($options['props'], $item));
+                        }
+                    }
+                }
+            }
+Horde::logMessage(print_r($list, true), __FILE__, __LINE__, PEAR_LOG_ERR);
+            return $list;
+        } else {
+            $path = trim($path, '/');
+            $pieces = explode('/', $path);
+            $items = $registry->callByPackage($pieces[0], 'browse', array('path' => $path, 'depth' => $options['depth']));
+            if ($items === false) {
+                // File not found
+                return $items;
+            }
+            if (is_a($items, 'PEAR_Error')) {
+                Horde::logMessage($items, __FILE__, __LINE__, PEAR_LOG_ERR);
+                return $items;
+            }
+            if (empty($items)) {
+                // No content exists at this level.
+                return array();
+            }
+            if (!is_array(reset($items))) {
+                /* A one-dimensional array means we have an actual object with
+                 * data to return to the client.
+                 */
+                $props = $this->_getProps($options['props'], $items);
+                $items = array(array('path' => $this->path,
+                                     'props' => $props));
+                return $items;
+            }
+
+            /* A directory full of objects has been returned. */
+            foreach ($items as $sub_path => $i) {
+                $props = $this->_getProps($options['props'], $i);
+
+                $item = array('path' => '/' . $sub_path,
+                              'props' => $props);
+                $list[] = $item;
+            }
+        }
+
+        return $list;
+    }
+
+    /**
+     * Given a set of requested properties ($reqprops) and an items holding
+     * properties, return a list of properties and values from the item that
+     * were requested.
+     *
+     * @param array  $reqprops List of requested properties
+     * @param array  $item     Item with properties to be filtered
+     *
+     * @return array           List of filtered properties and values
+     */
+    function _getProps($reqprops, $item)
+    {
+        $props = array();
+        $properties = array();
+        foreach ($reqprops as $prop) {
+            if (!isset($properties[$prop['xmlns']])) {
+                $properties[$prop['xmlns']] = array();
+            }
+            $properties[$prop['xmlns']][$prop['name']] = $prop['name'];
+        }
+        
+        // Handle certain standard properties specially
+        if (in_array('displayname', $properties['DAV:'])) {
+            $props[] = $this->mkprop('displayname', String::convertCharset($item['name'], NLS::getCharset(), 'UTF-8'));
+            unset($properties['DAV:']['displayname']);
+        }
+        if (in_array('getlastmodified', $properties['DAV:'])) {
+            $props[] = $this->mkprop('getlastmodified', empty($item['mtime']) ? time() : $item['mtime']);
+            unset($properties['DAV:']['getlastmodified']);
+        }
+        if (in_array('getcontenttype', $properties['DAV:'])) {
+            $props[] = $this->mkprop('getcontenttype', empty($item['mimetype']) ? 'application/octet-stream' : $item['mimetype']);
+            unset($properties['DAV:']['getcontenttype']);
+        }
+        if (in_array('getcontentlength', $properties['DAV:'])) {
+            if (empty($item['contentlength']) && empty($item['data'])) {
+                $size = 0;
+            } else {
+                $size = empty($item['contentlength']) ? strlen($item['data']) : $item['contentlength'];
+            }
+            $props[] = $this->mkprop('getcontentlength', $size);
+            unset($properties['DAV:']['getcontentlength']);
+        }
+        if (in_array('creationdate', $properties['DAV:'])) {
+            $props[] = $this->mkprop('creationdate', empty($item['created']) ? time() : $item['created']);
+            unset($properties['DAV:']['creationdate']);
+        }
+
+        if (isset($properties[self::CALDAVNS])) {
+            if (in_array('calendar-home-set', $properties[self::CALDAVNS]) &&
+                isset($item[self::CALDAVNS . ':calendar-home-set'])) {
+                $calendar_home_set = array();
+                foreach ($item[self::CALDAVNS . ':calendar-home-set'] as $calUrl) {
+                    $calendar_home_set[] = $this->mkprop('href', $calUrl);
+                }
+                $props[] = $this->mkprop('caldav', 'calendar-home-set', $calendar_home_set);
+                unset($properties[self::CALDAVNS]['calendar-home-set']);
+            }
+    
+            if (in_array('calendar-user-address-set', $properties[self::CALDAVNS]) &&
+                isset($item[self::CALDAVNS . ':calendar-user-address-set'])) {
+                $calendar_user_address_set = array();
+                foreach ($item[self::CALDAVNS . ':calendar-user-address-set'] as $userAddress) {
+                    $calendar_user_address_set[] = $this->mkprop('href', $userAddress);
+                }
+                $props[] = $this->mkprop('caldav', 'calendar-user-address-set', $calendar_user_address_set);
+                unset($properties[self::CALDAVNS]['calendar-user-address-set']);
+            }
+        }
+
+        // Handle any other requested properties genericly
+        $itemprops = array_keys($item);
+        foreach (array_keys($properties) as $xmlns) {
+            foreach ($properties[$xmlns] as $propname) {
+                if ($xmlns != 'DAV:') {
+                    $propname = $xmlns . ':' . $propname;
+                }
+                if (in_array($propname, $itemprops)) {
+                    $props[] = $this->mkprop($xmlns, $propname, $item[$propname]);
+                }
+            }
+        }
+
+        return $props;
+    }
+
+    /**
+     * Attempts to set a lock on a specified resource.
+     *
+     * @param array &$params  Reference to array of parameters.  These
+     *                        parameters should be overwritten with the lock
+     *                        information.
+     *
+     * @return int            HTTP status code
+     */
+    function LOCK(&$params)
+    {
+        if (!isset($GLOBALS['conf']['lock']['driver']) ||
+            $GLOBALS['conf']['lock']['driver'] == 'none') {
+            return 500;
+        }
+
+        if (empty($params['path'])) {
+            Horde::logMessage('Empty path supplied to LOCK()', __FILE__, __LINE__, PEAR_LOG_ERR);
+            return 403;
+        }
+        if ($params['path'] == '/') {
+            // Locks are always denied to the root directory
+            return 403;
+        }
+        if (isset($params['depth']) && $params['depth'] == 'infinity') {
+            // For now we categorically disallow recursive locks
+            return 403;
+        }
+
+        if (!is_array($params['timeout']) || count($params['timeout']) != 1) {
+            // Unexpected timeout parameter.  Assume 600 seconds.
+            $timeout = 600;
+        }
+        $tmp = explode('-', $params['timeout'][0]);
+        if (count($tmp) != 2) {
+            // Unexpected timeout parameter.  Assume 600 seconds.
+            $timeout = 600;
+        }
+        if (strtolower($tmp[0]) == 'second') {
+            $timeout = $tmp[1];
+        } else {
+            // Unexpected timeout parameter.  Assume 600 seconds.
+            $timeout = 600;
+        }
+
+        require_once 'Horde/Lock.php';
+        $locks = &Horde_Lock::singleton($GLOBALS['conf']['lock']['driver']);
+        if (is_a($locks, 'PEAR_Error')) {
+            Horde::logMessage($locks, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return 500;
+        }
+
+        $locktype = HORDE_LOCK_TYPE_SHARED;
+        if ($params['scope'] == 'exclusive') {
+            $locktype = HORDE_LOCK_TYPE_EXCLUSIVE;
+        }
+
+        $lockid = $locks->setLock(Auth::getAuth(), 'webdav', $params['path'],
+                                  $timeout, $locktype);
+
+        if (is_a($lockid, 'PEAR_Error')) {
+            Horde::logMessage($lockid, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return 500;
+        } elseif ($lockid === false) {
+            // Resource is already locked.
+            return 423;
+        }
+
+        $params['locktoken'] = $lockid;
+        $params['owner'] = Auth::getAuth();
+        $params['timeout'] = $timeout;
+
+        return "200";
+    }
+
+    /**
+     * Attempts to remove a specified lock.
+     *
+     * @param array &$params  Reference to array of parameters.  These
+     *                        parameters should be overwritten with the lock
+     *                        information.
+     *
+     * @return int            HTTP status code
+     */
+    function UNLOCK(&$params)
+    {
+        if (!isset($GLOBALS['conf']['lock']['driver']) ||
+            $GLOBALS['conf']['lock']['driver'] == 'none') {
+            return 500;
+        }
+
+        require_once 'Horde/Lock.php';
+        $locks = &Horde_Lock::singleton($GLOBALS['conf']['lock']['driver']);
+        if (is_a($locks, 'PEAR_Error')) {
+            Horde::logMessage($locks, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return 500;
+        }
+
+        $res = $locks->clearLock($params['token']);
+        if (is_a($res, 'PEAR_Error')) {
+            Horde::logMessage($res, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return 500;
+        } elseif ($res === false) {
+            Horde::logMessage('clearLock() returned false', __FILE__, __LINE__, PEAR_LOG_ERR);
+            // Something else has failed:  424 (Method Failure)
+            return 424;
+        }
+
+        // Lock cleared.  Use 204 (No Content) instead of 200 because there is
+        // no lock information to return to the client.
+        return 204;
+    }
+
+    function checkLock($resource)
+    {
+        if (!isset($GLOBALS['conf']['lock']['driver']) ||
+            $GLOBALS['conf']['lock']['driver'] == 'none') {
+            Horde::logMessage('WebDAV locking failed because no lock driver has been configured.', __FILE__, __LINE__, PEAR_LOG_WARNING);
+            return false;
+        }
+
+        require_once 'Horde/Lock.php';
+        $locks = &Horde_Lock::singleton($GLOBALS['conf']['lock']['driver']);
+        if (is_a($locks, 'PEAR_Error')) {
+            Horde::logMessage($locks, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return false;
+        }
+
+        $res =  $locks->getLocks('webdav', $resource);
+        if (is_a($res, 'PEAR_Error')) {
+            Horde::logMessage($res, __FILE__, __LINE__, PEAR_LOG_ERR);
+            return false;
+        }
+
+        if (empty($res)) {
+            // No locks found.
+            return $res;
+        }
+
+        // WebDAV only supports one lock.  Return the first lock.
+        $lock = reset($res);
+
+        // Format the array keys for HTTP_WebDAV_Server
+        $ret = array();
+        if ($lock['lock_type'] == HORDE_LOCK_TYPE_EXCLUSIVE) {
+            $ret['scope'] = 'exclusive';
+        } else {
+            $ret['scope'] = 'shared';
+        }
+        $ret['type'] = 'write';
+        $ret['expires'] = $lock['lock_expiry_timestamp'];
+        $ret['token'] = $lock['lock_id'];
+        $ret['depth'] = 1;
+
+        return $ret;
+    }
+
+    /**
+     * Check authentication. We always return true here since we
+     * handle permissions based on the resource that's requested, but
+     * we do record the authenticated user for later use.
+     *
+     * @param string $type      Authentication type, e.g. "basic" or "digest"
+     * @param string $username  Transmitted username.
+     * @param string $password  Transmitted password.
+     *
+     * @return boolean  Authentication status. Always true.
+     */
+    function check_auth($type, $username, $password)
+    {
+        $auth = &Auth::singleton($GLOBALS['conf']['auth']['driver']);
+        return $auth->authenticate($username, array('password' => $password));
+    }
+
+    /**
+     * Make sure the error code returned in the PEAR_Error object is a valid
+     * HTTP response code.
+     *
+     * This is necessary because in pre-Horde 3.2 apps the response codes are
+     * not sanitized.  This backward compatibility check can be removed when
+     * we drop support for pre-3.2 apps.  Intentionally, not every valid HTTP
+     * code is listed here.  Only common ones are here to reduce the
+     * possibility of an invalid code being confused with a valid HTTP code.
+     *
+     * @todo Remove for Horde 4.0
+     *
+     * @param integer $code  Status code to check for validity.
+     *
+     * @return integer  Either the original code if valid or 500 for internal
+     *                  server error.
+     */
+    function _checkHTTPcode($code)
+    {
+        $valid = array(200, // OK
+                       201, // Created
+                       202, // Accepted
+                       204, // No Content
+                       301, // Moved Permanently
+                       302, // Found
+                       304, // Not Modified
+                       307, // Temporary Redirect
+                       400, // Bad Request
+                       401, // Unauthorized
+                       403, // Forbidden
+                       404, // Not Found
+                       405, // Method Not Allowed
+                       406, // Not Acceptable
+                       408, // Request Timeout
+                       413, // Request Entity Too Large
+                       415, // Unsupported Media Type
+                       500, // Internal Server Error
+                       501, // Not Implemented
+                       503, // Service Unavailable
+        );
+        if (in_array($code, $valid)) {
+            return $code;
+        } else {
+            return 500;
+        }
+    }
+
+    /** 
+     * Serve WebDAV HTTP request
+     *
+     * dispatch WebDAV HTTP request to the apropriate method handler
+     * 
+     * @param  void
+     * @return void
+     */
+    function ServeRequest() 
+    {
+        // prevent warning in litmus check 'delete_fragment'
+        if (strstr($this->_SERVER["REQUEST_URI"], '#')) {
+            $this->http_status("400 Bad Request");
+            return;
+        }
+
+        // default uri is the complete request uri
+        $uri = "http";
+        if (isset($this->_SERVER["HTTPS"]) && $this->_SERVER["HTTPS"] === "on") {
+          $uri = "https";
+        }
+        $uri.= "://".$this->_SERVER["HTTP_HOST"].$this->_SERVER["SCRIPT_NAME"];
+        
+        // WebDAV has no concept of a query string and clients (including cadaver)
+        // seem to pass '?' unencoded, so we need to extract the path info out
+        // of the request URI ourselves
+        $path_info = substr($this->_SERVER["REQUEST_URI"], strlen($this->_SERVER["SCRIPT_NAME"]));
+
+        // just in case the path came in empty ...
+        if (empty($path_info)) {
+            $path_info = "/";
+        }
+
+        $this->base_uri = $uri;
+        $this->uri      = $uri . $path_info;
+
+        // set path
+        $this->path = $this->_urldecode($path_info);
+        if (!strlen($this->path)) {
+            if ($this->_SERVER["REQUEST_METHOD"] == "GET") {
+                // redirect clients that try to GET a collection
+                // WebDAV clients should never try this while
+                // regular HTTP clients might ...
+                header("Location: ".$this->base_uri."/");
+                return;
+            } else {
+                // if a WebDAV client didn't give a path we just assume '/'
+                $this->path = "/";
+            }
+        } 
+        
+        if (ini_get("magic_quotes_gpc")) {
+            $this->path = stripslashes($this->path);
+        }
+        
+        
+        // identify ourselves
+        if (empty($this->dav_powered_by)) {
+            header("X-Dav-Powered-By: PHP class: ".get_class($this));
+        } else {
+            header("X-Dav-Powered-By: ".$this->dav_powered_by);
+        }
+
+        // check authentication
+        // for the motivation for not checking OPTIONS requests on / see 
+        // http://pear.php.net/bugs/bug.php?id=5363
+        if ( (   !(($this->_SERVER['REQUEST_METHOD'] == 'OPTIONS') && ($this->path == "/")))
+             && (!$this->_check_auth())) {
+            // RFC2518 says we must use Digest instead of Basic
+            // but Microsoft Clients do not support Digest
+            // and we don't support NTLM and Kerberos
+            // so we are stuck with Basic here
+            header('WWW-Authenticate: Basic realm="'.($this->http_auth_realm).'"');
+
+            // Windows seems to require this being the last header sent
+            // (changed according to PECL bug #3138)
+            $this->http_status('401 Unauthorized');
+
+            return;
+        }
+        
+        // check 
+        if (! $this->_check_if_header_conditions()) {
+            return;
+        }
+        
+        // detect requested method names
+        $method  = strtolower($this->_SERVER["REQUEST_METHOD"]);
+        $wrapper = "http_".$method;
+        
+        // activate HEAD emulation by GET if no HEAD method found
+        if ($method == "head" && !method_exists($this, "head")) {
+            $method = "get";
+        }
+        
+        if (method_exists($this, $wrapper) && ($method == "options" || method_exists($this, $method))) {
+            $this->$wrapper();  // call method by name
+        } else { // method not found/implemented
+            if ($this->_SERVER["REQUEST_METHOD"] == "LOCK") {
+                $this->http_status("412 Precondition failed");
+            } else {
+                $this->http_status("405 Method not allowed");
+                header("Allow: ".join(", ", $this->_allow()));  // tell client what's allowed
+            }
+        }
+    }
+
+    // }}}
+
+    // {{{ abstract WebDAV methods 
+
+    // {{{ GET() 
+    /**
+     * GET implementation
+     *
+     * overload this method to retrieve resources from your server
+     * <br>
+     * 
+     *
+     * @abstract 
+     * @param array &$params Array of input and output parameters
+     * <br><b>input</b><ul>
+     * <li> path - 
+     * </ul>
+     * <br><b>output</b><ul>
+     * <li> size - 
+     * </ul>
+     * @returns int HTTP-Statuscode
+     */
+
+    /* abstract
+     function GET(&$params) 
+     {
+     // dummy entry for PHPDoc
+     } 
+    */
+
+    // }}}
+
+    // {{{ PUT() 
+    /**
+     * PUT implementation
+     *
+     * PUT implementation
+     *
+     * @abstract 
+     * @param array &$params
+     * @returns int HTTP-Statuscode
+     */
+    
+    /* abstract
+     function PUT() 
+     {
+     // dummy entry for PHPDoc
+     } 
+    */
+    
+    // }}}
+
+    // {{{ COPY() 
+
+    /**
+     * COPY implementation
+     *
+     * COPY implementation
+     *
+     * @abstract 
+     * @param array &$params
+     * @returns int HTTP-Statuscode
+     */
+    
+    /* abstract
+     function COPY() 
+     {
+     // dummy entry for PHPDoc
+     } 
+    */
+
+    // }}}
+
+    // {{{ MOVE() 
+
+    /**
+     * MOVE implementation
+     *
+     * MOVE implementation
+     *
+     * @abstract 
+     * @param array &$params
+     * @returns int HTTP-Statuscode
+     */
+    
+    /* abstract
+     function MOVE() 
+     {
+     // dummy entry for PHPDoc
+     } 
+    */
+
+    // }}}
+
+    // {{{ DELETE() 
+
+    /**
+     * DELETE implementation
+     *
+     * DELETE implementation
+     *
+     * @abstract 
+     * @param array &$params
+     * @returns int HTTP-Statuscode
+     */
+    
+    /* abstract
+     function DELETE() 
+     {
+     // dummy entry for PHPDoc
+     } 
+    */
+    // }}}
+
+    // {{{ PROPFIND() 
+
+    /**
+     * PROPFIND implementation
+     *
+     * PROPFIND implementation
+     *
+     * @abstract 
+     * @param array &$params
+     * @returns int HTTP-Statuscode
+     */
+    
+    /* abstract
+     function PROPFIND() 
+     {
+     // dummy entry for PHPDoc
+     } 
+    */
+
+    // }}}
+
+    // {{{ PROPPATCH() 
+
+    /**
+     * PROPPATCH implementation
+     *
+     * PROPPATCH implementation
+     *
+     * @abstract 
+     * @param array &$params
+     * @returns int HTTP-Statuscode
+     */
+    
+    /* abstract
+     function PROPPATCH() 
+     {
+     // dummy entry for PHPDoc
+     } 
+    */
+    // }}}
+
+    // {{{ LOCK() 
+
+    /**
+     * LOCK implementation
+     *
+     * LOCK implementation
+     *
+     * @abstract 
+     * @param array &$params
+     * @returns int HTTP-Statuscode
+     */
+    
+    /* abstract
+     function LOCK() 
+     {
+     // dummy entry for PHPDoc
+     } 
+    */
+    // }}}
+
+    // {{{ UNLOCK() 
+
+    /**
+     * UNLOCK implementation
+     *
+     * UNLOCK implementation
+     *
+     * @abstract 
+     * @param array &$params
+     * @returns int HTTP-Statuscode
+     */
+
+    /* abstract
+     function UNLOCK() 
+     {
+     // dummy entry for PHPDoc
+     } 
+    */
+    // }}}
+
+    // }}}
+
+    // {{{ other abstract methods 
+
+    // {{{ check_auth() 
+
+    /**
+     * check authentication
+     *
+     * overload this method to retrieve and confirm authentication information
+     *
+     * @abstract 
+     * @param string type Authentication type, e.g. "basic" or "digest"
+     * @param string username Transmitted username
+     * @param string passwort Transmitted password
+     * @returns bool Authentication status
+     */
+    
+    /* abstract
+     function checkAuth($type, $username, $password) 
+     {
+     // dummy entry for PHPDoc
+     } 
+    */
+    
+    // }}}
+
+    // {{{ checklock() 
+
+    /**
+     * check lock status for a resource
+     *
+     * overload this method to return shared and exclusive locks 
+     * active for this resource
+     *
+     * @abstract 
+     * @param string resource Resource path to check
+     * @returns array An array of lock entries each consisting
+     *                of 'type' ('shared'/'exclusive'), 'token' and 'timeout'
+     */
+    
+    /* abstract
+     function checklock($resource) 
+     {
+     // dummy entry for PHPDoc
+     } 
+    */
+
+    // }}}
+
+    // }}}
+
+    // {{{ WebDAV HTTP method wrappers 
+
+    // {{{ http_OPTIONS() 
+
+    /**
+     * OPTIONS method handler
+     *
+     * The OPTIONS method handler creates a valid OPTIONS reply
+     * including Dav: and Allowed: headers
+     * based on the implemented methods found in the actual instance
+     *
+     * @param  void
+     * @return void
+     */
+    function http_OPTIONS() 
+    {
+        // Microsoft clients default to the Frontpage protocol 
+        // unless we tell them to use WebDAV
+        header("MS-Author-Via: DAV");
+
+        // get allowed methods
+        $allow = $this->_allow();
+
+        // dav header
+        $dav = array(1);        // assume we are always dav class 1 compliant
+        if (isset($allow['LOCK'])) {
+            $dav[] = 2;         // dav class 2 requires that locking is supported 
+        }
+
+        // tell clients what we found
+        $this->http_status("200 OK");
+        header("DAV: "  .join(", ", $dav));
+        header("Allow: ".join(", ", $allow));
+
+        header("Content-length: 0");
+    }
+
+    // }}}
+
+
+    // {{{ http_PROPFIND() 
+
+    /**
+     * PROPFIND method handler
+     *
+     * @param  void
+     * @return void
+     */
+    function http_PROPFIND() 
+    {
+        $options = Array();
+        $files   = Array();
+
+        $options["path"] = $this->path;
+        
+        // search depth from header (default is "infinity)
+        if (isset($this->_SERVER['HTTP_DEPTH'])) {
+            $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
+        } else {
+            $options["depth"] = "infinity";
+        }       
+
+        // analyze request payload
+        $propinfo = $this->_parse_propfind("php://input");
+        if (!$this->parseSuccess) {
+            $this->http_status("400 Error");
+            return;
+        }
+        $options['props'] = $this->parseProps;
+
+        // call user handler
+        if (!$this->PROPFIND($options, $files)) {
+            $files = array("files" => array());
+            if (method_exists($this, "checkLock")) {
+                // is locked?
+                $lock = $this->checkLock($this->path);
+
+                if (is_array($lock) && count($lock)) {
+                    $created          = isset($lock['created'])  ? $lock['created']  : time();
+                    $modified         = isset($lock['modified']) ? $lock['modified'] : time();
+                    $files['files'][] = array("path"  => $this->_slashify($this->path),
+                                              "props" => array($this->mkprop("displayname",      $this->path),
+                                                               $this->mkprop("creationdate",     $created),
+                                                               $this->mkprop("getlastmodified",  $modified),
+                                                               $this->mkprop("resourcetype",     ""),
+                                                               $this->mkprop("getcontenttype",   ""),
+                                                               $this->mkprop("getcontentlength", 0))
+                                              );
+                }
+            }
+
+            if (empty($files['files'])) {
+                $this->http_status("404 Not Found");
+                return;
+            }
+        }
+        
+        $this->_xml = new Horde_Xml_Element('<D:multistatus xmlns:D="DAV:"/>');
+        // Microsoft Clients need this special namespace for date and
+        // time values
+        // FIXME: Unless we use this XMLNS on an attribute H_X_E will not send
+        // it with the output.
+        $this->_xml->registerNamespace('xmldata', "urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/");    
+        $this->_xml->registerNamespace('caldav', self::CALDAVNS);
+    
+        // now we loop over all returned file entries
+        foreach ($files["files"] as $filekey => $file) {
+            
+            // nothing to do if no properties were returend for a file
+            if (!isset($file["props"]) || !is_array($file["props"])) {
+                continue;
+            }
+            
+            // now loop over all returned properties
+            foreach ($file["props"] as $key => $prop) {
+                // as a convenience feature we do not require that user handlers
+                // restrict returned properties to the requested ones
+                // here we strip all unrequested entries out of the response
+                
+                switch($options['props']) {
+                case "all":
+                    // nothing to remove
+                    break;
+                    
+                case "names":
+                    // only the names of all existing properties were requested
+                    // so we remove all values
+                    unset($files["files"][$filekey]["props"][$key]["val"]);
+                    break;
+                    
+                default:
+                    $found = false;
+                    
+                    // search property name in requested properties 
+                    foreach ((array)$options["props"] as $reqprop) {
+                        if (!isset($reqprop["xmlns"])) {
+                            $reqprop["xmlns"] = "";
+                        }
+                        if (   $reqprop["name"]  == $prop["name"] 
+                               && $reqprop["xmlns"] == $prop["ns"]) {
+                            $found = true;
+                            break;
+                        }
+                    }
+                    
+                    // unset property and continue with next one if not found/requested
+                    if (!$found) {
+                        $files["files"][$filekey]["props"][$key]="";
+                        continue(2);
+                    }
+                    break;
+                }
+                
+                // namespace handling 
+                if (empty($prop["ns"])) continue; // no namespace
+                $ns = $prop["ns"]; 
+                if ($ns == "DAV:") continue; // default namespace
+                if (isset($this->ns_hash[$ns])) continue; // already known
+
+                // register namespace 
+                $ns_name = "ns".(count($this->ns_hash));
+                $this->ns_hash[$ns] = $ns_name;
+                $this->_xml->registerNamespace($ns_name, $ns);
+            }
+        
+            // we also need to add empty entries for properties that were requested
+            // but for which no values where returned by the user handler
+            if (is_array($options['props'])) {
+                foreach ($options["props"] as $reqprop) {
+                    if ($reqprop['name']=="") continue; // skip empty entries
+                    
+                    $found = false;
+                    
+                    if (!isset($reqprop["xmlns"])) {
+                        $reqprop["xmlns"] = "";
+                    }
+
+                    // check if property exists in result
+                    foreach ($file["props"] as $prop) {
+                        if (   $reqprop["name"]  == $prop["name"]
+                               && $reqprop["xmlns"] == $prop["ns"]) {
+                            $found = true;
+                            break;
+                        }
+                    }
+                    
+                    if (!$found) {
+                        if ($reqprop["xmlns"]==="DAV:" && $reqprop["name"]==="lockdiscovery") {
+                            // lockdiscovery is handled by the base class
+                            $files["files"][$filekey]["props"][] 
+                                = $this->mkprop("DAV:", 
+                                                "lockdiscovery", 
+                                                $this->lockdiscovery($files["files"][$filekey]['path']));
+                        } else {
+                            // add empty value for this property
+                            $files["files"][$filekey]["noprops"][] =
+                                $this->mkprop($reqprop["xmlns"], $reqprop["name"], "");
+
+                            // register property namespace if not known yet
+                            if ($reqprop["xmlns"] != "DAV:" && !isset($this->ns_hash[$reqprop["xmlns"]])) {
+                                $ns_name = "ns".(count($this->ns_hash));
+                                $this->ns_hash[$reqprop["xmlns"]] = $ns_name;
+                                $this->_xml->registerNamespace($ns_name, $reqprop['xmlns']);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        
+        // now we generate the reply header ...
+        $this->http_status("207 Multi-Status");
+        header('Content-Type: text/xml; charset="utf-8"');
+        
+        // ... and payload
+        foreach ($files["files"] as $file) {
+            // ignore empty or incomplete entries
+            if (!is_array($file) || empty($file) || !isset($file["path"])) continue;
+            $path = $file['path'];                  
+            if (!is_string($path) || $path==="") continue;
+
+            $xmldata = array('D:response' => array());
+            #echo " <D:response $ns_defs>\n";
+        
+            /* TODO right now the user implementation has to make sure
+             collections end in a slash, this should be done in here
+             by checking the resource attribute */
+            $href = $this->_mergePaths($this->_SERVER['SCRIPT_NAME'], $path);
+
+            /* minimal urlencoding is needed for the resource path */
+            $xmldata['D:response']['D:href'] = $this->_urlencode($href);
+            #echo "  <D:href>$href</D:href>\n";
+        
+            // report all found properties and their values (if any)
+            if (isset($file["props"]) && is_array($file["props"])) {
+                #echo "  <D:propstat>\n";
+                $i = 0;
+                $propstats = array($i => array('D:prop' => array()));
+                #echo "   <D:prop>\n";
+
+                foreach ($file["props"] as $key => $prop) {
+                    
+                    if (!is_array($prop)) continue;
+                    if (!isset($prop["name"])) continue;
+                    
+                    if (!isset($prop["val"]) || $prop["val"] === "" || $prop["val"] === false) {
+                        // empty properties (cannot use empty() for check as "0" is a legal value here)
+                        if ($prop["ns"]=="DAV:") {
+                            $propstats[$i]['D:prop']['D:' . $prop['name']] = '';
+                            #echo "     <D:$prop[name]/>\n";
+                        } else if (!empty($prop["ns"])) {
+                            $propstats[$i]['D:prop'][$this->ns_hash[$prop["ns"]].':'.$prop['name']] = '';
+                            #echo "     <".$this->ns_hash[$prop["ns"]].":$prop[name]/>\n";
+                        } else {
+                            $propstats[$i]['D:prop'][$prop['name'] . '#xmlns=""'] = '';
+                            #echo "     <$prop[name] xmlns=\"\"/>";
+                        }
+                    } else if ($prop["ns"] == "DAV:") {
+                        // some WebDAV properties need special treatment
+                        switch ($prop["name"]) {
+                        case "creationdate":
+                            $propstats[$i]['D:prop']['D:creationdate#xmldata:dt="dateTime.tz"'] = gmdate("Y-m-d\\TH:i:s\\Z", $prop['val']);
+                            #echo "     <D:creationdate ns0:dt=\"dateTime.tz\">"
+                            #    . gmdate("Y-m-d\\TH:i:s\\Z", $prop['val'])
+                            #    . "</D:creationdate>\n";
+                            break;
+                        case "getlastmodified":
+                            $propstats[$i]['D:prop']['D:getlastmodified#xmldata:dt="dateTime.rfc1123"'] = gmdate("D, d M Y H:i:s ", $prop['val']);
+                            #echo "     <D:getlastmodified ns0:dt=\"dateTime.rfc1123\">"
+                            #    . gmdate("D, d M Y H:i:s ", $prop['val'])
+                            #    . "GMT</D:getlastmodified>\n";
+                            break;
+                        case "resourcetype":
+                            $propstats[$i]['D:prop']['D:resourcetype']['D:'.$prop['val']] = '';
+                            #echo "     <D:resourcetype><D:$prop[val]/></D:resourcetype>\n";
+                            break;
+                        case "supportedlock":
+                            $propstats[$i]['D:prop']['D:supportedlock'] = $prop['val'];
+                            #echo "     <D:supportedlock>$prop[val]</D:supportedlock>\n";
+                            break;
+                        case "lockdiscovery":  
+                            $propstats[$i]['D:prop']['D:lockdiscovery'] = $prop['val'];
+                            #echo "     <D:lockdiscovery>\n";
+                            #echo $prop["val"];
+                            #echo "     </D:lockdiscovery>\n";
+                            break;
+                        // the following are non-standard Microsoft extensions to the DAV namespace
+                        case "lastaccessed":
+                            $propstats[$i]['D:prop']['D:lastaccessed#xmldata:dt="dateTime.rfc1123"'] = gmdate("D, d M Y H:i:s ", $prop['val']);
+                            #echo "     <D:lastaccessed ns0:dt=\"dateTime.rfc1123\">"
+                            #    . gmdate("D, d M Y H:i:s ", $prop['val'])
+                            #    . "GMT</D:lastaccessed>\n";
+                            break;
+                        case "ishidden":
+                            $propstats[$i]['D:prop']['D:ishidden'] = is_string($prop['val']) ? $prop['val'] : ($prop['val'] ? 'true' : 'false');
+                            #echo "     <D:ishidden>"
+                            #    . is_string($prop['val']) ? $prop['val'] : ($prop['val'] ? 'true' : 'false')
+                            #    . "</D:ishidden>\n";
+                            break;
+                        default:                                    
+                            $propstats[$i]['D:prop']['D:'. $prop['name']] = $prop['val'];
+                            #echo "     <D:$prop[name]>"
+                            #    . $this->_prop_encode(htmlspecialchars($prop['val']))
+                            #    .     "</D:$prop[name]>\n";                               
+                            break;
+                        }
+                    } else {
+                        list($key, $val) = $this->_prop2xml($prop);
+                        $propstats[$i]['D:prop'][$key] = $val;
+                        #echo $this->_prop2xml($prop);
+                    }
+                }
+
+                #echo "   </D:prop>\n";
+                $propstats[$i]['D:status'] = 'HTTP/1.1 200 OK';
+                #echo "   <D:status>HTTP/1.1 200 OK</D:status>\n";
+                #echo "  </D:propstat>\n";
+            }
+            // Increment to the next propstat stanza.
+            $i++;
+            
+            // now report all properties requested but not found
+            if (isset($file["noprops"])) {
+                #echo "  <D:propstat>\n";
+                $propstats[$i]['D:prop'] = array();
+                #echo "   <D:prop>\n";
+
+                foreach ($file["noprops"] as $key => $prop) {
+                    if ($prop["ns"] == "DAV:") {
+                        $propstats[$i]['D:prop']['D:' . $prop['name']] = '';
+                        #echo "     <D:$prop[name]/>\n";
+                    } else if ($prop["ns"] == "") {
+                        $propstats[$i]['D:prop'][$prop['name'] . '#xmlns=""'] = '';
+                        #echo "     <$prop[name] xmlns=\"\"/>\n";
+                    } else {
+                        $propstats[$i]['D:prop'][$this->ns_hash[$prop['ns']] . ':' . $prop['name']] = '';
+                        #echo "     <" . $this->ns_hash[$prop["ns"]] . ":$prop[name]/>\n";
+                    }
+                }
+
+                #echo "   </D:prop>\n";
+                $propstats[$i]['D:status'] = 'HTTP/1.1 404 Not Found';
+                #echo "   <D:status>HTTP/1.1 404 Not Found</D:status>\n";
+                #echo "  </D:propstat>\n";
+            }
+            
+            $xmldata['D:response']['D:propstat'] = $propstats;
+            #echo " </D:response>\n";
+        }
+        
+        #echo "</D:multistatus>\n";
+        echo $this->_xml->saveXml();
+    }
+
+    
+    // }}}
+    
+    // {{{ http_PROPPATCH() 
+
+    /**
+     * PROPPATCH method handler
+     *
+     * @param  void
+     * @return void
+     */
+    function http_PROPPATCH() 
+    {
+        if ($this->_check_lock_status($this->path)) {
+            $options = Array();
+
+            $options["path"] = $this->path;
+
+            $propinfo = $this->_parse_proppatch("php://input");
+            
+            if (!$this->parseSuccess) {
+                $this->http_status("400 Error");
+                return;
+            }
+            
+            $options['props'] = $this->parseProps;
+            
+            $responsedescr = $this->PROPPATCH($options);
+            
+            $this->http_status("207 Multi-Status");
+            header('Content-Type: text/xml; charset="utf-8"');
+            
+            echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
+
+            echo "<D:multistatus xmlns:D=\"DAV:\">\n";
+            echo " <D:response>\n";
+            echo "  <D:href>".$this->_urlencode($this->_mergePaths($this->_SERVER["SCRIPT_NAME"], $this->path))."</D:href>\n";
+
+            foreach ($options["props"] as $prop) {
+                echo "   <D:propstat>\n";
+                echo "    <D:prop><$prop[name] xmlns=\"$prop[ns]\"/></D:prop>\n";
+                echo "    <D:status>HTTP/1.1 $prop[status]</D:status>\n";
+                echo "   </D:propstat>\n";
+            }
+
+            if ($responsedescr) {
+                echo "  <D:responsedescription>".
+                    $this->_prop_encode(htmlspecialchars($responsedescr)).
+                    "</D:responsedescription>\n";
+            }
+
+            echo " </D:response>\n";
+            echo "</D:multistatus>\n";
+        } else {
+            $this->http_status("423 Locked");
+        }
+    }
+    
+    // }}}
+
+
+    // {{{ http_MKCOL() 
+
+    /**
+     * MKCOL method handler
+     *
+     * @param  void
+     * @return void
+     */
+    function http_MKCOL() 
+    {
+        $options = Array();
+
+        $options["path"] = $this->path;
+
+        $stat = $this->MKCOL($options);
+
+        $this->http_status($stat);
+    }
+
+    // }}}
+
+
+    // {{{ http_GET() 
+
+    /**
+     * GET method handler
+     *
+     * @param void
+     * @returns void
+     */
+    function http_GET() 
+    {
+        // TODO check for invalid stream
+        $options         = Array();
+        $options["path"] = $this->path;
+
+        $this->_get_ranges($options);
+
+        if (true === ($status = $this->GET($options))) {
+            if (!headers_sent()) {
+                $status = "200 OK";
+
+                if (!isset($options['mimetype'])) {
+                    $options['mimetype'] = "application/octet-stream";
+                }
+                header("Content-type: $options[mimetype]");
+                
+                if (isset($options['mtime'])) {
+                    header("Last-modified:".gmdate("D, d M Y H:i:s ", $options['mtime'])."GMT");
+                }
+                
+                if (isset($options['stream'])) {
+                    // GET handler returned a stream
+                    if (!empty($options['ranges']) && (0===fseek($options['stream'], 0, SEEK_SET))) {
+                        // partial request and stream is seekable 
+                        
+                        if (count($options['ranges']) === 1) {
+                            $range = $options['ranges'][0];
+                            
+                            if (isset($range['start'])) {
+                                fseek($options['stream'], $range['start'], SEEK_SET);
+                                if (feof($options['stream'])) {
+                                    $this->http_status("416 Requested range not satisfiable");
+                                    return;
+                                }
+
+                                if (isset($range['end'])) {
+                                    $size = $range['end']-$range['start']+1;
+                                    $this->http_status("206 partial");
+                                    header("Content-length: $size");
+                                    header("Content-range: $range[start]-$range[end]/"
+                                           . (isset($options['size']) ? $options['size'] : "*"));
+                                    while ($size && !feof($options['stream'])) {
+                                        $buffer = fread($options['stream'], 4096);
+                                        $size  -= $this->bytes($buffer);
+                                        echo $buffer;
+                                    }
+                                } else {
+                                    $this->http_status("206 partial");
+                                    if (isset($options['size'])) {
+                                        header("Content-length: ".($options['size'] - $range['start']));
+                                        header("Content-range: ".$range['start']."-".$range['end']."/"
+                                               . (isset($options['size']) ? $options['size'] : "*"));
+                                    }
+                                    fpassthru($options['stream']);
+                                }
+                            } else {
+                                header("Content-length: ".$range['last']);
+                                fseek($options['stream'], -$range['last'], SEEK_END);
+                                fpassthru($options['stream']);
+                            }
+                        } else {
+                            $this->_multipart_byterange_header(); // init multipart
+                            foreach ($options['ranges'] as $range) {
+                                // TODO what if size unknown? 500?
+                                if (isset($range['start'])) {
+                                    $from = $range['start'];
+                                    $to   = !empty($range['end']) ? $range['end'] : $options['size']-1; 
+                                } else {
+                                    $from = $options['size'] - $range['last']-1;
+                                    $to   = $options['size'] -1;
+                                }
+                                $total = isset($options['size']) ? $options['size'] : "*"; 
+                                $size  = $to - $from + 1;
+                                $this->_multipart_byterange_header($options['mimetype'], $from, $to, $total);
+
+
+                                fseek($options['stream'], $from, SEEK_SET);
+                                while ($size && !feof($options['stream'])) {
+                                    $buffer = fread($options['stream'], 4096);
+                                    $size  -= $this->bytes($buffer);
+                                    echo $buffer;
+                                }
+                            }
+                            $this->_multipart_byterange_header(); // end multipart
+                        }
+                    } else {
+                        // normal request or stream isn't seekable, return full content
+                        if (isset($options['size'])) {
+                            header("Content-length: ".$options['size']);
+                        }
+                        fpassthru($options['stream']);
+                        return; // no more headers
+                    }
+                } elseif (isset($options['data'])) {
+                    if (is_array($options['data'])) {
+                        // reply to partial request
+                    } else {
+                        header("Content-length: ".$this->bytes($options['data']));
+                        echo $options['data'];
+                    }
+                }
+            } 
+        } 
+
+        if (!headers_sent()) {
+            if (false === $status) {
+                $this->http_status("404 not found");
+            } else {
+                // TODO: check setting of headers in various code paths above
+                $this->http_status("$status");
+            }
+        }
+    }
+
+
+    /**
+     * parse HTTP Range: header
+     *
+     * @param  array options array to store result in
+     * @return void
+     */
+    function _get_ranges(&$options) 
+    {
+        // process Range: header if present
+        if (isset($this->_SERVER['HTTP_RANGE'])) {
+
+            // we only support standard "bytes" range specifications for now
+            if (preg_match('/bytes\s*=\s*(.+)/', $this->_SERVER['HTTP_RANGE'], $matches)) {
+                $options["ranges"] = array();
+
+                // ranges are comma separated
+                foreach (explode(",", $matches[1]) as $range) {
+                    // ranges are either from-to pairs or just end positions
+                    list($start, $end) = explode("-", $range);
+                    $options["ranges"][] = ($start==="") 
+                        ? array("last"=>$end) 
+                        : array("start"=>$start, "end"=>$end);
+                }
+            }
+        }
+    }
+
+    /**
+     * generate separator headers for multipart response
+     *
+     * first and last call happen without parameters to generate 
+     * the initial header and closing sequence, all calls inbetween
+     * require content mimetype, start and end byte position and
+     * optionaly the total byte length of the requested resource
+     *
+     * @param  string  mimetype
+     * @param  int     start byte position
+     * @param  int     end   byte position
+     * @param  int     total resource byte size
+     */
+    function _multipart_byterange_header($mimetype = false, $from = false, $to=false, $total=false) 
+    {
+        if ($mimetype === false) {
+            if (!isset($this->multipart_separator)) {
+                // initial
+
+                // a little naive, this sequence *might* be part of the content
+                // but it's really not likely and rather expensive to check 
+                $this->multipart_separator = "SEPARATOR_".md5(microtime());
+
+                // generate HTTP header
+                header("Content-type: multipart/byteranges; boundary=".$this->multipart_separator);
+            } else {
+                // final 
+
+                // generate closing multipart sequence
+                echo "\n--{$this->multipart_separator}--";
+            }
+        } else {
+            // generate separator and header for next part
+            echo "\n--{$this->multipart_separator}\n";
+            echo "Content-type: $mimetype\n";
+            echo "Content-range: $from-$to/". ($total === false ? "*" : $total);
+            echo "\n\n";
+        }
+    }
+
+            
+
+    // }}}
+
+    // {{{ http_HEAD() 
+
+    /**
+     * HEAD method handler
+     *
+     * @param  void
+     * @return void
+     */
+    function http_HEAD() 
+    {
+        $status          = false;
+        $options         = Array();
+        $options["path"] = $this->path;
+        
+        if (method_exists($this, "HEAD")) {
+            $status = $this->head($options);
+        } else if (method_exists($this, "GET")) {
+            ob_start();
+            $status = $this->GET($options);
+            if (!isset($options['size'])) {
+                $options['size'] = ob_get_length();
+            }
+            ob_end_clean();
+        }
+        
+        if (!isset($options['mimetype'])) {
+            $options['mimetype'] = "application/octet-stream";
+        }
+        header("Content-type: $options[mimetype]");
+        
+        if (isset($options['mtime'])) {
+            header("Last-modified:".gmdate("D, d M Y H:i:s ", $options['mtime'])."GMT");
+        }
+                
+        if (isset($options['size'])) {
+            header("Content-length: ".$options['size']);
+        }
+
+        if ($status === true)  $status = "200 OK";
+        if ($status === false) $status = "404 Not found";
+        
+        $this->http_status($status);
+    }
+
+    // }}}
+
+    // {{{ http_PUT() 
+
+    /**
+     * PUT method handler
+     *
+     * @param  void
+     * @return void
+     */
+    function http_PUT() 
+    {
+        if ($this->_check_lock_status($this->path)) {
+            $options                   = Array();
+            $options["path"]           = $this->path;
+            $options["content_length"] = $this->_SERVER["CONTENT_LENGTH"];
+
+            // get the Content-type 
+            if (isset($this->_SERVER["CONTENT_TYPE"])) {
+                // for now we do not support any sort of multipart requests
+                if (!strncmp($this->_SERVER["CONTENT_TYPE"], "multipart/", 10)) {
+                    $this->http_status("501 not implemented");
+                    echo "The service does not support mulipart PUT requests";
+                    return;
+                }
+                $options["content_type"] = $this->_SERVER["CONTENT_TYPE"];
+            } else {
+                // default content type if none given
+                $options["content_type"] = "application/octet-stream";
+            }
+
+            /* RFC 2616 2.6 says: "The recipient of the entity MUST NOT 
+             ignore any Content-* (e.g. Content-Range) headers that it 
+             does not understand or implement and MUST return a 501 
+             (Not Implemented) response in such cases."
+            */ 
+            foreach ($this->_SERVER as $key => $val) {
+                if (strncmp($key, "HTTP_CONTENT", 11)) continue;
+                switch ($key) {
+                case 'HTTP_CONTENT_ENCODING': // RFC 2616 14.11
+                    // TODO support this if ext/zlib filters are available
+                    $this->http_status("501 not implemented"); 
+                    echo "The service does not support '$val' content encoding";
+                    return;
+
+                case 'HTTP_CONTENT_LANGUAGE': // RFC 2616 14.12
+                    // we assume it is not critical if this one is ignored
+                    // in the actual PUT implementation ...
+                    $options["content_language"] = $val;
+                    break;
+
+                case 'HTTP_CONTENT_LENGTH':
+                    // defined on IIS and has the same value as CONTENT_LENGTH
+                    break;
+
+                case 'HTTP_CONTENT_LOCATION': // RFC 2616 14.14
+                    /* The meaning of the Content-Location header in PUT 
+                     or POST requests is undefined; servers are free 
+                     to ignore it in those cases. */
+                    break;
+
+                case 'HTTP_CONTENT_RANGE':    // RFC 2616 14.16
+                    // single byte range requests are supported
+                    // the header format is also specified in RFC 2616 14.16
+                    // TODO we have to ensure that implementations support this or send 501 instead
+                    if (!preg_match('@bytes\s+(\d+)-(\d+)/((\d+)|\*)@', $val, $matches)) {
+                        $this->http_status("400 bad request"); 
+                        echo "The service does only support single byte ranges";
+                        return;
+                    }
+                    
+                    $range = array("start"=>$matches[1], "end"=>$matches[2]);
+                    if (is_numeric($matches[3])) {
+                        $range["total_length"] = $matches[3];
+                    }
+                    $option["ranges"][] = $range;
+
+                    // TODO make sure the implementation supports partial PUT
+                    // this has to be done in advance to avoid data being overwritten
+                    // on implementations that do not support this ...
+                    break;
+
+                case 'HTTP_CONTENT_TYPE':
+                    // defined on IIS and has the same value as CONTENT_TYPE
+                    break;
+
+                case 'HTTP_CONTENT_MD5':      // RFC 2616 14.15
+                    // TODO: maybe we can just pretend here?
+                    $this->http_status("501 not implemented"); 
+                    echo "The service does not support content MD5 checksum verification"; 
+                    return;
+
+                default: 
+                    // any other unknown Content-* headers
+                    $this->http_status("501 not implemented"); 
+                    echo "The service does not support '$key'"; 
+                    return;
+                }
+            }
+
+            $options["stream"] = fopen("php://input", "r");
+
+            $stat = $this->PUT($options);
+
+            if ($stat === false) {
+                $stat = "403 Forbidden";
+            } else if (is_resource($stat) && get_resource_type($stat) == "stream") {
+                $stream = $stat;
+
+                $stat = $options["new"] ? "201 Created" : "204 No Content";
+
+                if (!empty($options["ranges"])) {
+                    // TODO multipart support is missing (see also above)
+                    if (0 == fseek($stream, $range[0]["start"], SEEK_SET)) {
+                        $length = $range[0]["end"]-$range[0]["start"]+1;
+                        if (!fwrite($stream, fread($options["stream"], $length))) {
+                            $stat = "403 Forbidden"; 
+                        }
+                    } else {
+                        $stat = "403 Forbidden"; 
+                    }
+                } else {
+                    while (!feof($options["stream"])) {
+                        if (false === fwrite($stream, fread($options["stream"], 4096))) {
+                            $stat = "403 Forbidden"; 
+                            break;
+                        }
+                    }
+                }
+
+                fclose($stream);            
+            } 
+
+            $this->http_status($stat);
+        } else {
+            $this->http_status("423 Locked");
+        }
+    }
+
+    // }}}
+
+
+    // {{{ http_DELETE() 
+
+    /**
+     * DELETE method handler
+     *
+     * @param  void
+     * @return void
+     */
+    function http_DELETE() 
+    {
+        // check RFC 2518 Section 9.2, last paragraph
+        if (isset($this->_SERVER["HTTP_DEPTH"])) {
+            if ($this->_SERVER["HTTP_DEPTH"] != "infinity") {
+                $this->http_status("400 Bad Request");
+                return;
+            }
+        }
+
+        // check lock status
+        if ($this->_check_lock_status($this->path)) {
+            // ok, proceed
+            $options         = Array();
+            $options["path"] = $this->path;
+
+            $stat = $this->DELETE($options);
+
+            $this->http_status($stat);
+        } else {
+            // sorry, its locked
+            $this->http_status("423 Locked");
+        }
+    }
+
+    // }}}
+
+    // {{{ http_COPY() 
+
+    /**
+     * COPY method handler
+     *
+     * @param  void
+     * @return void
+     */
+    function http_COPY() 
+    {
+        // no need to check source lock status here 
+        // destination lock status is always checked by the helper method
+        $this->_copymove("copy");
+    }
+
+    // }}}
+
+    // {{{ http_MOVE() 
+
+    /**
+     * MOVE method handler
+     *
+     * @param  void
+     * @return void
+     */
+    function http_MOVE() 
+    {
+        if ($this->_check_lock_status($this->path)) {
+            // destination lock status is always checked by the helper method
+            $this->_copymove("move");
+        } else {
+            $this->http_status("423 Locked");
+        }
+    }
+
+    // }}}
+
+
+    // {{{ http_LOCK() 
+
+    /**
+     * LOCK method handler
+     *
+     * @param  void
+     * @return void
+     */
+    function http_LOCK() 
+    {
+        $options         = Array();
+        $options["path"] = $this->path;
+        
+        if (isset($this->_SERVER['HTTP_DEPTH'])) {
+            $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
+        } else {
+            $options["depth"] = "infinity";
+        }
+        
+        if (isset($this->_SERVER["HTTP_TIMEOUT"])) {
+            $options["timeout"] = explode(",", $this->_SERVER["HTTP_TIMEOUT"]);
+        }
+        
+        if (empty($this->_SERVER['CONTENT_LENGTH']) && !empty($this->_SERVER['HTTP_IF'])) {
+            // check if locking is possible
+            if (!$this->_check_lock_status($this->path)) {
+                $this->http_status("423 Locked");
+                return;
+            }
+
+            // refresh lock
+            $options["locktoken"] = substr($this->_SERVER['HTTP_IF'], 2, -2);
+            $options["update"]    = $options["locktoken"];
+
+            // setting defaults for required fields, LOCK() SHOULD overwrite these
+            $options['owner']     = "unknown";
+            $options['scope']     = "exclusive";
+            $options['type']      = "write";
+
+
+            $stat = $this->LOCK($options);
+        } else {
+            // extract lock request information from request XML payload
+            $lockinfo = $this->_parse_lockinfo("php://input");
+            if (!$lockinfo->parseSuccess) {
+                $this->http_status("400 bad request"); 
+            }
+
+            // check if locking is possible
+            if (!$this->_check_lock_status($this->path, $this->lockscope === "shared")) {
+                $this->http_status("423 Locked");
+                return;
+            }
+
+            // new lock 
+            $options["scope"]     = $this->lockscope;
+            $options["type"]      = $this->locktype;
+            $options["owner"]     = $this->owner;            
+            $options["locktoken"] = $this->_new_locktoken();
+            
+            $stat = $this->LOCK($options);              
+        }
+        
+        if (is_bool($stat)) {
+            $http_stat = $stat ? "200 OK" : "423 Locked";
+        } else {
+            $http_stat = (string)$stat;
+        }
+        $this->http_status($http_stat);
+        
+        if ($http_stat{0} == 2) { // 2xx states are ok 
+            if ($options["timeout"]) {
+                // if multiple timeout values were given we take the first only
+                if (is_array($options["timeout"])) {
+                    reset($options["timeout"]);
+                    $options["timeout"] = current($options["timeout"]);
+                }
+                // if the timeout is numeric only we need to reformat it
+                if (is_numeric($options["timeout"])) {
+                    // more than a million is considered an absolute timestamp
+                    // less is more likely a relative value
+                    if ($options["timeout"]>1000000) {
+                        $timeout = "Second-".($options['timeout']-time());
+                    } else {
+                        $timeout = "Second-$options[timeout]";
+                    }
+                } else {
+                    // non-numeric values are passed on verbatim,
+                    // no error checking is performed here in this case
+                    // TODO: send "Infinite" on invalid timeout strings?
+                    $timeout = $options["timeout"];
+                }
+            } else {
+                $timeout = "Infinite";
+            }
+            
+            header('Content-Type: text/xml; charset="utf-8"');
+            header("Lock-Token: <$options[locktoken]>");
+            echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
+            echo "<D:prop xmlns:D=\"DAV:\">\n";
+            echo " <D:lockdiscovery>\n";
+            echo "  <D:activelock>\n";
+            echo "   <D:lockscope><D:$options[scope]/></D:lockscope>\n";
+            echo "   <D:locktype><D:$options[type]/></D:locktype>\n";
+            echo "   <D:depth>$options[depth]</D:depth>\n";
+            echo "   <D:owner>$options[owner]</D:owner>\n";
+            echo "   <D:timeout>$timeout</D:timeout>\n";
+            echo "   <D:locktoken><D:href>$options[locktoken]</D:href></D:locktoken>\n";
+            echo "  </D:activelock>\n";
+            echo " </D:lockdiscovery>\n";
+            echo "</D:prop>\n\n";
+        }
+    }
+    
+
+    // }}}
+
+    // {{{ http_UNLOCK() 
+
+    /**
+     * UNLOCK method handler
+     *
+     * @param  void
+     * @return void
+     */
+    function http_UNLOCK() 
+    {
+        $options         = Array();
+        $options["path"] = $this->path;
+
+        if (isset($this->_SERVER['HTTP_DEPTH'])) {
+            $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
+        } else {
+            $options["depth"] = "infinity";
+        }
+
+        // strip surrounding <>
+        $options["token"] = substr(trim($this->_SERVER["HTTP_LOCK_TOKEN"]), 1, -1);  
+
+        // call user method
+        $stat = $this->UNLOCK($options);
+
+        $this->http_status($stat);
+    }
+
+    // }}}
+
+    // }}}
+
+    // {{{ _copymove() 
+
+    function _copymove($what) 
+    {
+        $options         = Array();
+        $options["path"] = $this->path;
+
+        if (isset($this->_SERVER["HTTP_DEPTH"])) {
+            $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
+        } else {
+            $options["depth"] = "infinity";
+        }
+
+        $http_header_host = preg_replace("/:80$/", "", $this->_SERVER["HTTP_HOST"]);
+
+        $url  = parse_url($this->_SERVER["HTTP_DESTINATION"]);
+        $path = urldecode($url["path"]);
+
+        if (isset($url["host"])) {
+            // TODO check url scheme, too
+            $http_host = $url["host"];
+            if (isset($url["port"]) && $url["port"] != 80)
+                $http_host.= ":".$url["port"];
+        } else {
+            // only path given, set host to self
+            $http_host == $http_header_host;
+        }
+
+        if ($http_host == $http_header_host &&
+            !strncmp($this->_SERVER["SCRIPT_NAME"], $path,
+                     strlen($this->_SERVER["SCRIPT_NAME"]))) {
+            $options["dest"] = substr($path, strlen($this->_SERVER["SCRIPT_NAME"]));
+            if (!$this->_check_lock_status($options["dest"])) {
+                $this->http_status("423 Locked");
+                return;
+            }
+
+        } else {
+            $options["dest_url"] = $this->_SERVER["HTTP_DESTINATION"];
+        }
+
+        // see RFC 2518 Sections 9.6, 8.8.4 and 8.9.3
+        if (isset($this->_SERVER["HTTP_OVERWRITE"])) {
+            $options["overwrite"] = $this->_SERVER["HTTP_OVERWRITE"] == "T";
+        } else {
+            $options["overwrite"] = true;
+        }
+
+        $stat = $this->$what($options);
+        $this->http_status($stat);
+    }
+
+    // }}}
+
+    // {{{ _allow() 
+
+    /**
+     * check for implemented HTTP methods
+     *
+     * @param  void
+     * @return array something
+     */
+    function _allow() 
+    {
+        // OPTIONS is always there
+        $allow = array("OPTIONS" =>"OPTIONS");
+
+        // all other METHODS need both a http_method() wrapper
+        // and a method() implementation
+        // the base class supplies wrappers only
+        foreach (get_class_methods($this) as $method) {
+            if (!strncmp("http_", $method, 5)) {
+                $method = strtoupper(substr($method, 5));
+                if (method_exists($this, $method)) {
+                    $allow[$method] = $method;
+                }
+            }
+        }
+
+        // we can emulate a missing HEAD implemetation using GET
+        if (isset($allow["GET"]))
+            $allow["HEAD"] = "HEAD";
+
+        // no LOCK without checklok()
+        if (!method_exists($this, "checklock")) {
+            unset($allow["LOCK"]);
+            unset($allow["UNLOCK"]);
+        }
+
+        return $allow;
+    }
+
+    // }}}
+
+    /**
+     * helper for property element creation
+     *
+     * @param  string  XML namespace (optional)
+     * @param  string  property name
+     * @param  string  property value
+     * @return array   property array
+     */
+    function mkprop() 
+    {
+        $args = func_get_args();
+        if (count($args) == 3) {
+            return array($args[0] . ':' . $args[1] => $args[2]);
+        } else {
+            return array("D:" . $args[0] => $args[1]);
+        }
+    }
+
+    // {{{ _check_auth 
+
+    /**
+     * check authentication if check is implemented
+     * 
+     * @param  void
+     * @return bool  true if authentication succeded or not necessary
+     */
+    function _check_auth() 
+    {
+        $auth_type = isset($this->_SERVER["AUTH_TYPE"]) 
+            ? $this->_SERVER["AUTH_TYPE"] 
+            : null;
+
+        $auth_user = isset($this->_SERVER["PHP_AUTH_USER"]) 
+            ? $this->_SERVER["PHP_AUTH_USER"] 
+            : null;
+
+        $auth_pw   = isset($this->_SERVER["PHP_AUTH_PW"]) 
+            ? $this->_SERVER["PHP_AUTH_PW"] 
+            : null;
+
+        if (method_exists($this, "checkAuth")) {
+            // PEAR style method name
+            return $this->checkAuth($auth_type, $auth_user, $auth_pw);
+        } else if (method_exists($this, "check_auth")) {
+            // old (pre 1.0) method name
+            return $this->check_auth($auth_type, $auth_user, $auth_pw);
+        } else {
+            // no method found -> no authentication required
+            return true;
+        }
+    }
+
+    // }}}
+
+    // {{{ UUID stuff 
+    
+    /**
+     * create a new opaque lock token as defined in RFC2518
+     *
+     * @param  void
+     * @return string  new RFC2518 opaque lock token
+     */
+    function _new_locktoken() 
+    {
+        return "opaquelocktoken:" . ((string)new Horde_Support_Uuid());
+    }
+
+    // }}}
+
+    // {{{ WebDAV If: header parsing 
+
+    /**
+     * 
+     *
+     * @param  string  header string to parse
+     * @param  int     current parsing position
+     * @return array   next token (type and value)
+     */
+    function _if_header_lexer($string, &$pos) 
+    {
+        // skip whitespace
+        while (ctype_space($string{$pos})) {
+            ++$pos;
+        }
+
+        // already at end of string?
+        if (strlen($string) <= $pos) {
+            return false;
+        }
+
+        // get next character
+        $c = $string{$pos++};
+
+        // now it depends on what we found
+        switch ($c) {
+        case "<":
+            // URIs are enclosed in <...>
+            $pos2 = strpos($string, ">", $pos);
+            $uri  = substr($string, $pos, $pos2 - $pos);
+            $pos  = $pos2 + 1;
+            return array("URI", $uri);
+
+        case "[":
+            //Etags are enclosed in [...]
+            if ($string{$pos} == "W") {
+                $type = "ETAG_WEAK";
+                $pos += 2;
+            } else {
+                $type = "ETAG_STRONG";
+            }
+            $pos2 = strpos($string, "]", $pos);
+            $etag = substr($string, $pos + 1, $pos2 - $pos - 2);
+            $pos  = $pos2 + 1;
+            return array($type, $etag);
+
+        case "N":
+            // "N" indicates negation
+            $pos += 2;
+            return array("NOT", "Not");
+
+        default:
+            // anything else is passed verbatim char by char
+            return array("CHAR", $c);
+        }
+    }
+
+    /** 
+     * parse If: header
+     *
+     * @param  string  header string
+     * @return array   URIs and their conditions
+     */
+    function _if_header_parser($str) 
+    {
+        $pos  = 0;
+        $len  = strlen($str);
+        $uris = array();
+
+        // parser loop
+        while ($pos < $len) {
+            // get next token
+            $token = $this->_if_header_lexer($str, $pos);
+
+            // check for URI
+            if ($token[0] == "URI") {
+                $uri   = $token[1]; // remember URI
+                $token = $this->_if_header_lexer($str, $pos); // get next token
+            } else {
+                $uri = "";
+            }
+
+            // sanity check
+            if ($token[0] != "CHAR" || $token[1] != "(") {
+                return false;
+            }
+
+            $list  = array();
+            $level = 1;
+            $not   = "";
+            while ($level) {
+                $token = $this->_if_header_lexer($str, $pos);
+                if ($token[0] == "NOT") {
+                    $not = "!";
+                    continue;
+                }
+                switch ($token[0]) {
+                case "CHAR":
+                    switch ($token[1]) {
+                    case "(":
+                        $level++;
+                        break;
+                    case ")":
+                        $level--;
+                        break;
+                    default:
+                        return false;
+                    }
+                    break;
+
+                case "URI":
+                    $list[] = $not."<$token[1]>";
+                    break;
+
+                case "ETAG_WEAK":
+                    $list[] = $not."[W/'$token[1]']>";
+                    break;
+
+                case "ETAG_STRONG":
+                    $list[] = $not."['$token[1]']>";
+                    break;
+
+                default:
+                    return false;
+                }
+                $not = "";
+            }
+
+            if (isset($uris[$uri]) && is_array($uris[$uri])) {
+                $uris[$uri] = array_merge($uris[$uri], $list);
+            } else {
+                $uris[$uri] = $list;
+            }
+        }
+
+        return $uris;
+    }
+
+    /**
+     * check if conditions from "If:" headers are meat 
+     *
+     * the "If:" header is an extension to HTTP/1.1
+     * defined in RFC 2518 section 9.4
+     *
+     * @param  void
+     * @return void
+     */
+    function _check_if_header_conditions() 
+    {
+        if (isset($this->_SERVER["HTTP_IF"])) {
+            $this->_if_header_uris =
+                $this->_if_header_parser($this->_SERVER["HTTP_IF"]);
+
+            foreach ($this->_if_header_uris as $uri => $conditions) {
+                if ($uri == "") {
+                    $uri = $this->uri;
+                }
+                // all must match
+                $state = true;
+                foreach ($conditions as $condition) {
+                    // lock tokens may be free form (RFC2518 6.3)
+                    // but if opaquelocktokens are used (RFC2518 6.4)
+                    // we have to check the format (litmus tests this)
+                    if (!strncmp($condition, "<opaquelocktoken:", strlen("<opaquelocktoken"))) {
+                        if (!preg_match('/^<opaquelocktoken:[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}>$/', $condition)) {
+                            $this->http_status("423 Locked");
+                            return false;
+                        }
+                    }
+                    if (!$this->_check_uri_condition($uri, $condition)) {
+                        $this->http_status("412 Precondition failed");
+                        $state = false;
+                        break;
+                    }
+                }
+
+                // any match is ok
+                if ($state == true) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Check a single URI condition parsed from an if-header
+     *
+     * Check a single URI condition parsed from an if-header
+     *
+     * @abstract 
+     * @param string $uri URI to check
+     * @param string $condition Condition to check for this URI
+     * @returns bool Condition check result
+     */
+    function _check_uri_condition($uri, $condition) 
+    {
+        // not really implemented here, 
+        // implementations must override
+
+        // a lock token can never be from the DAV: scheme
+        // litmus uses DAV:no-lock in some tests
+        if (!strncmp("<DAV:", $condition, 5)) {
+            return false;
+        }
+
+        return true;
+    }
+
+
+    /**
+     * 
+     *
+     * @param  string  path of resource to check
+     * @param  bool    exclusive lock?
+     */
+    function _check_lock_status($path, $exclusive_only = false) 
+    {
+        // FIXME depth -> ignored for now
+        if (method_exists($this, "checkLock")) {
+            // is locked?
+            $lock = $this->checkLock($path);
+
+            // ... and lock is not owned?
+            if (is_array($lock) && count($lock)) {
+                // FIXME doesn't check uri restrictions yet
+                if (!isset($this->_SERVER["HTTP_IF"]) || !strstr($this->_SERVER["HTTP_IF"], $lock["token"])) {
+                    if (!$exclusive_only || ($lock["scope"] !== "shared"))
+                        return false;
+                }
+            }
+        }
+        return true;
+    }
+
+
+    // }}}
+
+
+    /**
+     * Generate lockdiscovery reply from checklock() result
+     *
+     * @param   string  resource path to check
+     * @return  string  lockdiscovery response
+     */
+    function lockdiscovery($path) 
+    {
+        // no lock support without checklock() method
+        if (!method_exists($this, "checklock")) {
+            return "";
+        }
+
+        // collect response here
+        $activelocks = "";
+
+        // get checklock() reply
+        $lock = $this->checklock($path);
+
+        // generate <activelock> block for returned data
+        if (is_array($lock) && count($lock)) {
+            // check for 'timeout' or 'expires'
+            if (!empty($lock["expires"])) {
+                $timeout = "Second-".($lock["expires"] - time());
+            } else if (!empty($lock["timeout"])) {
+                $timeout = "Second-$lock[timeout]";
+            } else {
+                $timeout = "Infinite";
+            }
+
+            // genreate response block
+            $activelocks.= "
+              <D:activelock>
+               <D:lockscope><D:$lock[scope]/></D:lockscope>
+               <D:locktype><D:$lock[type]/></D:locktype>
+               <D:depth>$lock[depth]</D:depth>
+               <D:owner>$lock[owner]</D:owner>
+               <D:timeout>$timeout</D:timeout>
+               <D:locktoken><D:href>$lock[token]</D:href></D:locktoken>
+              </D:activelock>
+             ";
+        }
+
+        // return generated response
+        return $activelocks;
+    }
+
+    /**
+     * set HTTP return status and mirror it in a private header
+     *
+     * @param  string  status code and message
+     * @return void
+     */
+    function http_status($status) 
+    {
+        // simplified success case
+        if ($status === true) {
+            $status = "200 OK";
+        }
+
+        // remember status
+        $this->_http_status = $status;
+
+        // generate HTTP status response
+        header("HTTP/1.1 $status");
+        header("X-WebDAV-Status: $status", true);
+    }
+
+    /**
+     * private minimalistic version of PHP urlencode()
+     *
+     * only blanks, percent and XML special chars must be encoded here
+     * full urlencode() encoding confuses some clients ...
+     *
+     * @param  string  URL to encode
+     * @return string  encoded URL
+     */
+    function _urlencode($url) 
+    {
+        return strtr($url, array(" "=>"%20",
+                                 "%"=>"%25",
+                                 "&"=>"%26",
+                                 "<"=>"%3C",
+                                 ">"=>"%3E",
+                                 ));
+    }
+
+    /**
+     * private version of PHP urldecode
+     *
+     * not really needed but added for completenes
+     *
+     * @param  string  URL to decode
+     * @return string  decoded URL
+     */
+    function _urldecode($path) 
+    {
+        return rawurldecode($path);
+    }
+
+    /**
+     * UTF-8 encode property values if not already done so
+     *
+     * @param  string  text to encode
+     * @return string  utf-8 encoded text
+     */
+    function _prop_encode($text) 
+    {
+        switch (strtolower($this->_prop_encoding)) {
+        case "utf-8":
+            return $text;
+        case "iso-8859-1":
+        case "iso-8859-15":
+        case "latin-1":
+        default:
+            return utf8_encode($text);
+        }
+    }
+
+    /**
+     * Slashify - make sure path ends in a slash
+     *
+     * @param   string directory path
+     * @returns string directory path wiht trailing slash
+     */
+    function _slashify($path) 
+    {
+        if ($path[strlen($path)-1] != '/') {
+            $path = $path."/";
+        }
+        return $path;
+    }
+
+    /**
+     * Unslashify - make sure path doesn't in a slash
+     *
+     * @param   string directory path
+     * @returns string directory path wihtout trailing slash
+     */
+    function _unslashify($path) 
+    {
+        if ($path[strlen($path)-1] == '/') {
+            $path = substr($path, 0, strlen($path) -1);
+        }
+        return $path;
+    }
+
+    /**
+     * Merge two paths, make sure there is exactly one slash between them
+     *
+     * @param  string  parent path
+     * @param  string  child path
+     * @return string  merged path
+     */
+    function _mergePaths($parent, $child) 
+    {
+        if ($child{0} == '/') {
+            return $this->_unslashify($parent).$child;
+        } else {
+            return $this->_slashify($parent).$child;
+        }
+    }
+
+    function _prop2xml($prop)
+    {
+Horde::logMessage(print_r($prop, true), __FILE__, __LINE__, PEAR_LOG_ERR);
+        $res = array();
+
+        // properties from namespaces != "DAV:" or without any namespace 
+        if ($prop["ns"]) {
+            $key = $this->ns_hash[$prop['ns']] . ':' . $prop['name'];
+            #$res .= "<" . $this->ns_hash[$prop["ns"]] . ":$prop[name]>";
+        } else {
+            $key = $prop['name'] . '#xmlns=""';
+            #$res .= "<$prop[name] xmlns=\"\">";
+        }
+
+        // Check for and handle nested properties
+        if (is_array($prop['val'] && isset($prop['val']['name']))) {
+            // This is a single nested property
+            $res[$key] = $this->_prop2xml($prop['val']);
+        } elseif (is_array($prop['val'])) {
+            // This nested property has multiple values
+            foreach ($prop['val'] as $entry) {
+                $res[$key] = $this->_prop2xml($entry);
+            }
+        } else {
+            // This is a simple property value
+            $res[$key] = $prop['val'];
+        }
+
+        return $res;
+    }
+    
+    /**
+     * mbstring.func_overload save strlen version: counting the bytes not the chars
+     *
+     * @param string $str
+     * @return int
+     */
+    function bytes($str)
+    {
+       static $func_overload;
+       
+       if (is_null($func_overload))
+       {
+               $func_overload = @extension_loaded('mbstring') ? ini_get('mbstring.func_overload') : 0;
+       }
+       return $func_overload & 2 ? mb_strlen($str,'ascii') : strlen($str);
+    }
+
+
+    function _parse_propfind($path) 
+    {
+        // success state flag
+        $this->parseSuccess = true;
+        
+        // property storage array
+        $this->parseProps = array();
+
+        // internal tag depth counter
+        $this->parseDepth = 0;
+
+        // remember if any input was parsed
+        $had_input = false;
+
+        // open input stream
+        $f_in = fopen($path, "r");
+        if (!$f_in) {
+            $this->parseSuccess = false;
+            return;
+        }
+
+        // create XML parser
+        $xml_parser = xml_parser_create_ns("UTF-8", " ");
+
+        // set tag and data handlers
+        xml_set_element_handler($xml_parser,
+                                array(&$this, "_startPropinfoElement"),
+                                array(&$this, "_endPropinfoElement"));
+
+        // we want a case sensitive parser
+        xml_parser_set_option($xml_parser, 
+                              XML_OPTION_CASE_FOLDING, false);
+
+
+        // parse input
+        while ($this->parseSuccess && !feof($f_in)) {
+            $line = fgets($f_in);
+            if (is_string($line)) {
+                $had_input = true;
+                $this->parseSuccess &= xml_parse($xml_parser, $line, false);
+            }
+        } 
+        
+        // finish parsing
+        if ($had_input) {
+            $this->parseSuccess &= xml_parse($xml_parser, "", true);
+        }
+
+        // free parser
+        xml_parser_free($xml_parser);
+        
+        // close input stream
+        fclose($f_in);
+
+        // if no input was parsed it was a request
+        if(!count($this->parseProps)) $this->parseProps = "all"; // default
+    }
+    
+
+    /**
+     * start tag handler
+     * 
+     * @access private
+     * @param  resource  parser
+     * @param  string    tag name
+     * @param  array     tag attributes
+     */
+    function _startPropinfoElement($parser, $name, $attrs) 
+    {
+        // name space handling
+        if (strstr($name, " ")) {
+            list($ns, $tag) = explode(" ", $name);
+            if ($ns == "")
+                $this->parseSuccess = false;
+        } else {
+            $ns  = "";
+            $tag = $name;
+        }
+
+        // special tags at level 1: <allprop> and <propname>
+        if ($this->parseDepth == 1) {
+            if ($tag == "allprop")
+                $this->parseProps = "all";
+
+            if ($tag == "propname")
+                $this->parseProps = "names";
+        }
+
+        // requested properties are found at level 2
+        if ($this->parseDepth == 2) {
+            $prop = array("name" => $tag);
+            if ($ns)
+                $prop["xmlns"] = $ns;
+            $this->parseProps[] = $prop;
+        }
+
+        // increment depth count
+        $this->parseDepth++;
+    }
+    
+
+    /**
+     * end tag handler
+     * 
+     * @access private
+     * @param  resource  parser
+     * @param  string    tag name
+     */
+    function _endPropinfoElement($parser, $name) 
+    {
+        // here we only need to decrement the depth count
+        $this->parseDepth--;
+    }
+
+    function _parse_lockinfo($path) 
+    {
+        // we assume success unless problems occur
+        $this->parseSuccess = true;
+
+        // remember if any input was parsed
+        $had_input = false;
+        
+        // open stream
+        $f_in = fopen($path, "r");
+        if (!$f_in) {
+            $this->parseSuccess = false;
+            return;
+        }
+
+        // create namespace aware parser
+        $xml_parser = xml_parser_create_ns("UTF-8", " ");
+
+        // set tag and data handlers
+        xml_set_element_handler($xml_parser,
+                                array(&$this, "_startLockElement"),
+                                array(&$this, "_endLockElement"));
+        xml_set_character_data_handler($xml_parser,
+                                       array(&$this, "_lockData"));
+
+        // we want a case sensitive parser
+        xml_parser_set_option($xml_parser,
+                              XML_OPTION_CASE_FOLDING, false);
+
+        // parse input
+        while ($this->parseSuccess && !feof($f_in)) {
+            $line = fgets($f_in);
+            if (is_string($line)) {
+                $had_input = true;
+                $this->parseSuccess &= xml_parse($xml_parser, $line, false);
+            }
+        } 
+
+        // finish parsing
+        if ($had_input) {
+            $this->parseSuccess &= xml_parse($xml_parser, "", true);
+        }
+
+        // check if required tags where found
+        $this->parseSuccess &= !empty($this->locktype);
+        $this->parseSuccess &= !empty($this->lockscope);
+
+        // free parser resource
+        xml_parser_free($xml_parser);
+
+        // close input stream
+        fclose($f_in);      
+    }
+    
+
+    /**
+     * tag start handler
+     *
+     * @param  resource  parser
+     * @param  string    tag name
+     * @param  array     tag attributes
+     * @return void
+     * @access private
+     */
+    function _startLockElement($parser, $name, $attrs) 
+    {
+        // namespace handling
+        if (strstr($name, " ")) {
+            list($ns, $tag) = explode(" ", $name);
+        } else {
+            $ns  = "";
+            $tag = $name;
+        }
+        
+  
+        if ($this->collect_owner) {
+            // everything within the <owner> tag needs to be collected
+            $ns_short = "";
+            $ns_attr  = "";
+            if ($ns) {
+                if ($ns == "DAV:") {
+                    $ns_short = "D:";
+                } else {
+                    $ns_attr = " xmlns='$ns'";
+                }
+            }
+            $this->owner .= "<$ns_short$tag$ns_attr>";
+        } else if ($ns == "DAV:") {
+            // parse only the essential tags
+            switch ($tag) {
+            case "write":
+                $this->locktype = $tag;
+                break;
+            case "exclusive":
+            case "shared":
+                $this->lockscope = $tag;
+                break;
+            case "owner":
+                $this->collect_owner = true;
+                break;
+            }
+        }
+    }
+    
+    /**
+     * data handler
+     *
+     * @param  resource  parser
+     * @param  string    data
+     * @return void
+     * @access private
+     */
+    function _lockData($parser, $data) 
+    {
+        // only the <owner> tag has data content
+        if ($this->collect_owner) {
+            $this->owner .= $data;
+        }
+    }
+
+    /**
+     * tag end handler
+     *
+     * @param  resource  parser
+     * @param  string    tag name
+     * @return void
+     * @access private
+     */
+    function _endLockElement($parser, $name) 
+    {
+        // namespace handling
+        if (strstr($name, " ")) {
+            list($ns, $tag) = explode(" ", $name);
+        } else {
+            $ns  = "";
+            $tag = $name;
+        }
+
+        // <owner> finished?
+        if (($ns == "DAV:") && ($tag == "owner")) {
+            $this->collect_owner = false;
+        }
+
+        // within <owner> we have to collect everything
+        if ($this->collect_owner) {
+            $ns_short = "";
+            $ns_attr  = "";
+            if ($ns) {
+                if ($ns == "DAV:") {
+                    $ns_short = "D:";
+                } else {
+                    $ns_attr = " xmlns='$ns'";
+                }
+            }
+            $this->owner .= "</$ns_short$tag$ns_attr>";
+        }
+    }
+
+    function _parse_proppatch($path) 
+    {
+        $this->parseSuccess = true;
+
+        $this->parseDepth = 0;
+        $this->parseProps = array();
+        $had_input = false;
+
+        $f_in = fopen($path, "r");
+        if (!$f_in) {
+            $this->parseSuccess = false;
+            return;
+        }
+
+        $xml_parser = xml_parser_create_ns("UTF-8", " ");
+
+        xml_set_element_handler($xml_parser,
+                                array(&$this, "_startProppatchElement"),
+                                array(&$this, "_endProppatchElement"));
+
+        xml_set_character_data_handler($xml_parser,
+                                       array(&$this, "_proppatchData"));
+
+        xml_parser_set_option($xml_parser,
+                              XML_OPTION_CASE_FOLDING, false);
+
+        while($this->parseSuccess && !feof($f_in)) {
+            $line = fgets($f_in);
+            if (is_string($line)) {
+                $had_input = true;
+                $this->parseSuccess &= xml_parse($xml_parser, $line, false);
+            }
+        } 
+        
+        if($had_input) {
+            $this->parseSuccess &= xml_parse($xml_parser, "", true);
+        }
+
+        xml_parser_free($xml_parser);
+
+        fclose($f_in);
+    }
+
+    /**
+     * tag start handler
+     *
+     * @param  resource  parser
+     * @param  string    tag name
+     * @param  array     tag attributes
+     * @return void
+     * @access private
+     */
+    function _startProppatchElement($parser, $name, $attrs) 
+    {
+        if (strstr($name, " ")) {
+            list($ns, $tag) = explode(" ", $name);
+            if ($ns == "")
+                $this->parseSuccess = false;
+        } else {
+            $ns = "";
+            $tag = $name;
+        }
+
+        if ($this->parseDepth == 1) {
+            $this->mode = $tag;
+        } 
+
+        if ($this->parseDepth == 3) {
+            $prop = array("name" => $tag);
+            $this->current = array("name" => $tag, "ns" => $ns, "status"=> 200);
+            if ($this->mode == "set") {
+                $this->current["val"] = "";     // default set val
+            }
+        }
+
+        if ($this->parseDepth >= 4) {
+            $this->current["val"] .= "<$tag";
+            if (isset($attr)) {
+                foreach ($attr as $key => $val) {
+                    $this->current["val"] .= ' '.$key.'="'.str_replace('"','&quot;', $val).'"';
+                }
+            }
+            $this->current["val"] .= ">";
+        }
+
+        
+
+        $this->parseDepth++;
+    }
+
+    /**
+     * tag end handler
+     *
+     * @param  resource  parser
+     * @param  string    tag name
+     * @return void
+     * @access private
+     */
+    function _endProppatchElement($parser, $name) 
+    {
+        if (strstr($name, " ")) {
+            list($ns, $tag) = explode(" ", $name);
+            if ($ns == "")
+                $this->parseSuccess = false;
+        } else {
+            $ns = "";
+            $tag = $name;
+        }
+
+        $this->parseDepth--;
+
+        if ($this->parseDepth >= 4) {
+            $this->current["val"] .= "</$tag>";
+        }
+
+        if ($this->parseDepth == 3) {
+            if (isset($this->current)) {
+                $this->parseProps[] = $this->current;
+                unset($this->current);
+            }
+        }
+    }
+
+    /**
+     * input data handler
+     *
+     * @param  resource  parser
+     * @param  string    data
+     * @return void
+     * @access private
+     */
+    function _proppatchData($parser, $data) 
+    {
+        if (isset($this->current)) {
+            $this->current["val"] .= $data;
+        }
+    }
+
+} 
diff --git a/framework/RPC/RPC/xmlrpc.php b/framework/RPC/RPC/xmlrpc.php
new file mode 100644 (file)
index 0000000..d1dca95
--- /dev/null
@@ -0,0 +1,153 @@
+<?php
+/**
+ * The Horde_RPC_xmlrpc class provides an XMLRPC implementation of the
+ * Horde RPC system.
+ *
+ * $Horde: framework/RPC/RPC/xmlrpc.php,v 1.24 2009/01/06 17:49:38 jan Exp $
+ *
+ * Copyright 2002-2009 The Horde Project (http://www.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>
+ * @since   Horde 3.0
+ * @package Horde_RPC
+ */
+class Horde_RPC_xmlrpc extends Horde_RPC {
+
+    /**
+     * Resource handler for the XMLRPC server.
+     *
+     * @var resource
+     */
+    var $_server;
+
+    /**
+     * XMLRPC server constructor
+     *
+     * @access private
+     */
+    function Horde_RPC_xmlrpc()
+    {
+        parent::Horde_RPC();
+
+        $this->_server = xmlrpc_server_create();
+
+        foreach ($GLOBALS['registry']->listMethods() as $method) {
+            xmlrpc_server_register_method($this->_server, str_replace('/', '.', $method), array('Horde_RPC_xmlrpc', '_dispatcher'));
+        }
+    }
+
+    /**
+     * 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)
+    {
+        $response = null;
+        return xmlrpc_server_call_method($this->_server, $request, $response);
+    }
+
+    /**
+     * 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 'Method "' . $method . '" is not defined';
+        }
+
+        $result = $registry->call($method, $params);
+        if (is_a($result, 'PEAR_Error')) {
+            $result = array('faultCode' => (int)$result->getCode(),
+                            'faultString' => $result->getMessage());
+        }
+
+        return $result;
+    }
+
+    /**
+     * Builds an XMLRPC request and sends it to the XMLRPC server.
+     *
+     * This statically called method is actually the XMLRPC client.
+     *
+     * @param string $url     The path to the XMLRPC server on the called host.
+     * @param string $method  The method to call.
+     * @param array $params   A hash containing any necessary parameters for
+     *                        the method call.
+     * @param $options  Optional associative array of parameters which can be:
+     *                  user           - Basic Auth username
+     *                  pass           - Basic Auth password
+     *                  proxy_host     - Proxy server host
+     *                  proxy_port     - Proxy server port
+     *                  proxy_user     - Proxy auth username
+     *                  proxy_pass     - Proxy auth password
+     *                  timeout        - Connection timeout in seconds.
+     *                  allowRedirects - Whether to follow redirects or not
+     *                  maxRedirects   - Max number of redirects to follow
+     *
+     * @return mixed            The returned result from the method or a PEAR
+     *                          error object on failure.
+     */
+    function request($url, $method, $params = null, $options = array())
+    {
+        $options['method'] = 'POST';
+        $language = isset($GLOBALS['language']) ? $GLOBALS['language'] :
+                    (isset($_SERVER['LANG']) ? $_SERVER['LANG'] : '');
+
+        if (!isset($options['timeout'])) {
+            $options['timeout'] = 5;
+        }
+        if (!isset($options['allowRedirects'])) {
+            $options['allowRedirects'] = true;
+            $options['maxRedirects'] = 3;
+        }
+        if (!isset($options['proxy_host']) && !empty($GLOBALS['conf']['http']['proxy']['proxy_host'])) {
+            $options = array_merge($options, $GLOBALS['conf']['http']['proxy']);
+        }
+
+        require_once 'HTTP/Request.php';
+        $http = new HTTP_Request($url, $options);
+        if (!empty($language)) {
+            $http->addHeader('Accept-Language', $language);
+        }
+        $http->addHeader('User-Agent', 'Horde RPC client');
+        $http->addHeader('Content-Type', 'text/xml');
+        $http->addRawPostData(xmlrpc_encode_request($method, $params));
+
+        $result = $http->sendRequest();
+        if (is_a($result, 'PEAR_Error')) {
+            return $result;
+        } elseif ($http->getResponseCode() != 200) {
+            return PEAR::raiseError('Request couldn\'t be answered. Returned errorcode: "' . $http->getResponseCode(), 'horde.error');
+        } elseif (strpos($http->getResponseBody(), '<?xml') === false) {
+            return PEAR::raiseError('No valid XML data returned', 'horde.error', null, null, $http->getResponseBody());
+        } else {
+            $response = @xmlrpc_decode(substr($http->getResponseBody(), strpos($http->getResponseBody(), '<?xml')));
+            if (is_array($response) && isset($response['faultString'])) {
+                return PEAR::raiseError($response['faultString'], 'horde.error');
+            } elseif (is_array($response) && isset($response[0]) &&
+                      is_array($response[0]) && isset($response[0]['faultString'])) {
+                return PEAR::raiseError($response[0]['faultString'], 'horde.error');
+            }
+            return $response;
+        }
+    }
+
+}
diff --git a/framework/RPC/docs/examples/soap.php b/framework/RPC/docs/examples/soap.php
new file mode 100644 (file)
index 0000000..bf428ba
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+/**
+ * @package Horde_RPC
+ */
+
+die("Please configure the URL, username, and password, and then remove this line.\n");
+
+require_once 'Horde/RPC.php';
+
+// SOAP endpoint
+$rpc_endpoint = 'http://example.com/horde/rpc.php';
+
+// SOAP method to call
+$rpc_method = 'calendar.listCalendars';
+
+// SOAP options, usually username and password
+$rpc_options = array(
+    'user' => '',
+    'pass' => '',
+    'namespace' => 'urn:horde',
+);
+
+$result = Horde_RPC::request(
+    'soap',
+    $GLOBALS['rpc_endpoint'],
+    $GLOBALS['rpc_method'],
+    array(),
+    $GLOBALS['rpc_options']);
+
+var_dump($result);
diff --git a/framework/RPC/docs/examples/soap.pl b/framework/RPC/docs/examples/soap.pl
new file mode 100644 (file)
index 0000000..6ba8bef
--- /dev/null
@@ -0,0 +1,16 @@
+#!/usr/bin/perl -w
+
+die("Please configure the URL, username, and password, and then remove this line.\n");
+
+use SOAP::Lite;
+use Data::Dumper;
+
+my $proxy = 'http://username:password@example.com/horde/rpc.php';
+
+my $slite = SOAP::Lite
+    -> proxy($proxy)
+    -> call('calendar.listCalendars');
+
+my $status = $slite->result;
+
+print Data::Dumper->Dump($status);
diff --git a/framework/RPC/docs/examples/xmlrpc.php b/framework/RPC/docs/examples/xmlrpc.php
new file mode 100644 (file)
index 0000000..e9849f0
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+/**
+ * @package Horde_RPC
+ */
+
+die("Please configure the URL, username, and password, and then remove this line.\n");
+
+require_once 'Horde/RPC.php';
+
+// XML-RPC endpoint
+$rpc_endpoint = 'http://example.com/horde/rpc.php';
+
+// XML-RPC method to call
+$rpc_method = 'calendar.listCalendars';
+
+// XML-RPC options, usually username and password
+$rpc_options = array(
+    'user' => '',
+    'pass' => '',
+);
+
+$result = Horde_RPC::request(
+    'xmlrpc',
+    $GLOBALS['rpc_endpoint'],
+    $GLOBALS['rpc_method'],
+    array(),
+    $GLOBALS['rpc_options']);
+
+var_dump($result);
diff --git a/framework/RPC/docs/examples/xmlrpc.pl b/framework/RPC/docs/examples/xmlrpc.pl
new file mode 100644 (file)
index 0000000..5e404a0
--- /dev/null
@@ -0,0 +1,16 @@
+#!/usr/bin/perl -w
+
+die("Please configure the URL, username, and password, and then remove this line.\n");
+
+use XMLRPC::Lite;
+use Data::Dumper;
+
+my $proxy = 'http://username:password@example.com/horde/rpc.php';
+
+my $xlite = XMLRPC::Lite
+    -> proxy($proxy)
+    -> call('calendar.listCalendars');
+
+my $status = $xlite->result;
+
+print Data::Dumper->Dump($status);
diff --git a/framework/RPC/package.xml b/framework/RPC/package.xml
new file mode 100644 (file)
index 0000000..f6f69cc
--- /dev/null
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Horde_RPC</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde RPC API</summary>
+ <description>The Horde_RPC:: class provides a common abstracted interface to
+various remote methods of accessing Horde functionality.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>jan</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2006-05-08</date>
+ <time>23:04:55</time>
+ <version>
+  <release>0.0.2</release>
+  <api>0.0.2</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>Converted to package.xml 2.0 for pear.horde.org
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="RPC">
+    <file baseinstalldir="/Horde" name="PhpSoap.php" role="php" />
+    <file baseinstalldir="/Horde" name="jsonrpc.php" role="php" />
+    <file baseinstalldir="/Horde" name="phpgw.php" role="php" />
+    <file baseinstalldir="/Horde" name="soap.php" role="php" />
+    <file baseinstalldir="/Horde" name="syncml.php" role="php" />
+    <file baseinstalldir="/Horde" name="syncml_wbxml.php" role="php" />
+    <file baseinstalldir="/Horde" name="webdav.php" role="php" />
+    <file baseinstalldir="/Horde" name="xmlrpc.php" role="php" />
+   </dir> <!-- /RPC -->
+   <dir name="tests">
+    <file baseinstalldir="/Horde" name="rpc-test.php" role="test" />
+   </dir> <!-- /tests -->
+   <file baseinstalldir="/Horde" name="RPC.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>4.3.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.0b1</min>
+   </pearinstaller>
+   <package>
+    <name>SOAP</name>
+    <channel>pear.php.net</channel>
+    <min>0.8RC3</min>
+   </package>
+   <package>
+    <name>Horde_Framework</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Support</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <extension>
+    <name>xmlrpc</name>
+   </extension>
+  </required>
+ </dependencies>
+ <phprelease />
+ <changelog>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <date>2003-11-25</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial release as a Horde package
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/RPC/tests/rpc-test.php b/framework/RPC/tests/rpc-test.php
new file mode 100644 (file)
index 0000000..9e86afb
--- /dev/null
@@ -0,0 +1,94 @@
+#!/usr/bin/php
+<?php
+/**
+ * $Horde: framework/RPC/tests/rpc-test.php,v 1.8 2006/06/15 22:53:21 chuck Exp $
+ *
+ * @package Horde_RPC
+ */
+
+@define('HORDE_BASE', dirname(dirname(dirname(dirname(__FILE__)))));
+$_SERVER['SERVER_NAME'] = 'localhost';
+$_SERVER['SERVER_PORT'] = 80;
+require_once HORDE_BASE . '/lib/base.php';
+require_once 'Horde/RPC.php';
+
+if (!isset($argv) || count($argv) < 2) {
+    die("Can't read arguments.\n");
+}
+
+$testno = $argv[1];
+$user   = @$argv[2];
+$pass   = @$argv[3];
+
+switch ($testno) {
+case 0:
+    $response = Horde_RPC::request('xmlrpc', Horde::url('rpc.php', true, -1),
+                                   'system.listMethods', null,
+                                   array('user' => $user, 'pass' => $pass));
+    break;
+
+case 1:
+    $response = Horde_RPC::request('xmlrpc', Horde::url('rpc.php', true, -1),
+                                   'system.describeMethods', array('tasks.list'),
+                                   array('user' => $user, 'pass' => $pass));
+    break;
+
+case 2:
+    $response = Horde_RPC::request('xmlrpc', Horde::url('rpc.php', true, -1),
+                                   'tasks.list', array(0),
+                                   array('user' => $user, 'pass' => $pass));
+    break;
+
+case 3:
+    $response = Horde_RPC::request('xmlrpc', 'http://dev.horde.org/horde/rpc.php',
+                                   'system.listMethods', null,
+                                   array('user' => $user, 'pass' => $pass));
+    break;
+
+case 4:
+    $response = Horde_RPC::request('xmlrpc', 'http://pear.php.net/xmlrpc.php',
+                                   'package.listAll');
+    break;
+
+case 5:
+    $response = Horde_RPC::request('soap', 'http://api.google.com/search/beta2',
+                                   'doGoogleSearch',
+                                   array('key' => '5a/mF/FQFHKTD4vgNxfFeODwtLdifPPq',
+                                         'q' => 'Horde IMP',
+                                         'start' => 0,
+                                         'maxResults' => 10,
+                                         'filter' => true,
+                                         'restrict' => '',
+                                         'safeSearch' => false,
+                                         'lr' => '',
+                                         'ie' => 'iso-8859-1',
+                                         'oe' => 'iso-8859-1'),
+                                   array('namespace' => 'urn:GoogleSearch'));
+    break;
+
+case 6:
+    $response = Horde_RPC::request('soap', Horde::url('rpc.php', true, -1),
+                                   'tasks.list', array(0, 0),
+                                   array('namespace' => 'urn:horde',
+                                         'user' => $user,
+                                         'pass' => $pass));
+    break;
+
+}
+
+if (is_a($response, 'PEAR_Error')) {
+    echo "===error======\n";
+    echo $response->getMessage();
+    echo "\n";
+    $info = $response->getUserInfo();
+    if (is_string($info)) {
+        echo strtr($info, array_flip(get_html_translation_table(HTML_ENTITIES)));
+    } else {
+        var_dump($info);
+    }
+    echo "\n==============\n";
+} else {
+    echo "===value======\n";
+    var_dump($response);
+    echo "==============\n";
+}