فهرست منبع

Feature/php xmlrpc replacement (#596)

* Use Native php xmlrpc calls since php-xmlrpc is no longer maintained

* Use Native php xmlrpc calls since php-xmlrpc is no longer maintained

* Working XmlRPC Alternative Implementation

* Working XmlRPC Alternative Implementation
OwN-3m-All 3 سال پیش
والد
کامیت
5d48962d91
37فایلهای تغییر یافته به همراه10354 افزوده شده و 77 حذف شده
  1. 133 0
      includes/XmlRPC-bootstrap.php
  2. 519 0
      includes/XmlRPC.php
  3. 80 76
      includes/lib_remote.php
  4. 36 0
      includes/phpxmlrpc/Autoloader.php
  5. 2058 0
      includes/phpxmlrpc/Client.php
  6. 388 0
      includes/phpxmlrpc/Encoder.php
  7. 7 0
      includes/phpxmlrpc/Exception.php
  8. 12 0
      includes/phpxmlrpc/Exception/FaultResponseException.php
  9. 22 0
      includes/phpxmlrpc/Exception/HttpException.php
  10. 7 0
      includes/phpxmlrpc/Exception/NoSuchMethodException.php
  11. 12 0
      includes/phpxmlrpc/Exception/ParsingException.php
  12. 4 0
      includes/phpxmlrpc/Exception/PhpXmlrpcException.php
  13. 12 0
      includes/phpxmlrpc/Exception/ServerException.php
  14. 12 0
      includes/phpxmlrpc/Exception/StateErrorException.php
  15. 12 0
      includes/phpxmlrpc/Exception/TransportException.php
  16. 12 0
      includes/phpxmlrpc/Exception/TypeErrorException.php
  17. 12 0
      includes/phpxmlrpc/Exception/ValueErrorException.php
  18. 7 0
      includes/phpxmlrpc/Exception/XmlException.php
  19. 7 0
      includes/phpxmlrpc/Exception/XmlRpcException.php
  20. 395 0
      includes/phpxmlrpc/Helper/Charset.php
  21. 64 0
      includes/phpxmlrpc/Helper/Date.php
  22. 283 0
      includes/phpxmlrpc/Helper/Http.php
  23. 42 0
      includes/phpxmlrpc/Helper/Interop.php
  24. 123 0
      includes/phpxmlrpc/Helper/Logger.php
  25. 1098 0
      includes/phpxmlrpc/Helper/XMLParser.php
  26. 318 0
      includes/phpxmlrpc/PhpXmlRpc.php
  27. 563 0
      includes/phpxmlrpc/Request.php
  28. 338 0
      includes/phpxmlrpc/Response.php
  29. 1612 0
      includes/phpxmlrpc/Server.php
  30. 27 0
      includes/phpxmlrpc/Traits/CharsetEncoderAware.php
  31. 40 0
      includes/phpxmlrpc/Traits/DeprecationLogger.php
  32. 27 0
      includes/phpxmlrpc/Traits/LoggerAware.php
  33. 28 0
      includes/phpxmlrpc/Traits/ParserAware.php
  34. 45 0
      includes/phpxmlrpc/Traits/PayloadBearer.php
  35. 741 0
      includes/phpxmlrpc/Value.php
  36. 1258 0
      includes/phpxmlrpc/Wrapper.php
  37. 0 1
      install.php

+ 133 - 0
includes/XmlRPC-bootstrap.php

@@ -0,0 +1,133 @@
+<?php
+
+/**
+ * @author Gaetano Giunta
+ * @copyright (c) 2020 G. Giunta
+ * @license code licensed under the BSD License: see license.txt
+ */
+include_once 'XmlRPC.php';
+use PhpXmlRpc\Polyfill\XmlRpc as p;
+
+if (!function_exists('xmlrpc_decode')) {
+    /**
+     * @param string $xml
+     * @param string $encoding
+     * @return mixed
+     */
+    function xmlrpc_decode($xml, $encoding = "iso-8859-1") { return p\XmlRpc::xmlrpc_decode($xml, $encoding); }
+}
+
+if (!function_exists('xmlrpc_decode_request')) {
+    /**
+     * @param string $xml
+     * @param string $method
+     * @param string $encoding
+     * @return mixed
+     */
+    function xmlrpc_decode_request($xml, &$method, $encoding = "iso-8859-1") { return p\XmlRpc::xmlrpc_decode_request($xml, $method, $encoding); }
+}
+
+if (!function_exists('xmlrpc_encode')) {
+    /**
+     * @param mixed $value
+     * @return string
+     */
+    function xmlrpc_encode($value) { return p\XmlRpc::xmlrpc_encode($value); }
+}
+
+if (!function_exists('xmlrpc_encode_request')) {
+    /**
+     * @param string $method
+     * @param mixed $params
+     * @param array $output_options
+     * @return string
+     */
+    function xmlrpc_encode_request($method, $params, $output_options = array()) { return p\XmlRpc::xmlrpc_encode_request($method, $params, $output_options); }
+}
+
+if (!function_exists('xmlrpc_get_type')) {
+    /**
+     * @param mixed $value
+     * @return string
+     */
+    function xmlrpc_get_type($value) { return p\XmlRpc::xmlrpc_get_type($value); }
+}
+
+if (!function_exists('xmlrpc_is_fault')) {
+    /**
+     * @param array $arg
+     * @return bool
+     */
+    function xmlrpc_is_fault($arg) { return p\XmlRpc::xmlrpc_is_fault($arg); }
+}
+
+if (!function_exists('xmlrpc_parse_method_descriptions')) {
+    /**
+     * @param string $xml
+     * @return array
+     */
+    function xmlrpc_parse_method_descriptions($xml) { return p\XmlRpc::xmlrpc_parse_method_descriptions($xml); }
+}
+
+if (!function_exists('xmlrpc_server_add_introspection_data')) {
+    /**
+     * @param \PhpXmlrpc\Server $server
+     * @param array $desc
+     * @return int
+     */
+    function xmlrpc_server_add_introspection_data($server, $desc) { return p\XmlRpc::xmlrpc_server_add_introspection_data($server, $desc); }
+}
+
+if (!function_exists('xmlrpc_server_call_method')) {
+    /**
+     * @param \PhpXmlrpc\Server $server
+     * @param string $xml
+     * @param mixed $user_data
+     * @param array $output_options
+     * @return string
+     */
+    function xmlrpc_server_call_method($server, $xml, $user_data, $output_options = array()) { return p\XmlRpc::xmlrpc_server_call_method($server, $xml, $user_data, $output_options); }
+}
+
+if (!function_exists('xmlrpc_server_create')) {
+    /**
+     * @return \PhpXmlrpc\Server
+     */
+    function xmlrpc_server_create() { return p\XmlRpc::xmlrpc_server_create(); }
+}
+
+if (!function_exists('xmlrpc_server_destroy')) {
+    /**
+     * @param \PhpXmlrpc\Server $server
+     * @return int
+     */
+    function xmlrpc_server_destroy($server) { return p\XmlRpc::xmlrpc_server_destroy($server); }
+}
+
+if (!function_exists('xmlrpc_server_register_introspection_callback')) {
+    /**
+     * @param \PhpXmlrpc\Server $server
+     * @param string $function
+     * @return bool
+     */
+    function xmlrpc_server_register_introspection_callback($server, $function) { return p\XmlRpc::xmlrpc_server_register_introspection_callback($server, $function); }
+}
+
+if (!function_exists('xmlrpc_server_register_method')) {
+    /**
+     * @param \PhpXmlrpc\Server $server
+     * @param string $method_name
+     * @param string $function
+     * @return bool
+     */
+    function xmlrpc_server_register_method($server, $method_name, $function) { return p\XmlRpc::xmlrpc_server_register_method($server, $method_name, $function); }
+}
+
+if (!function_exists('xmlrpc_set_type')) {
+    /**
+     * @param string $value
+     * @param string $type
+     * @return bool
+     */
+    function xmlrpc_set_type(&$value, $type) { return p\XmlRpc::xmlrpc_set_type($value, $type); }
+}

+ 519 - 0
includes/XmlRPC.php

@@ -0,0 +1,519 @@
+<?php
+
+/**
+ * (Try to) implement the same API of the PHP native XMLRPC extension, so that
+ * projects relying on it can be ported to php installs where the extension is
+ * missing.
+ *
+ * @author Gaetano Giunta
+ * @copyright (c) 2020-2021 G. Giunta
+ * @license code licensed under the BSD License: see license.txt
+ *
+ * Known differences from the observed behaviour of the PHP extension:
+ * Definitely to fix:
+ * - the $output_options argument in xmlrpc_encode_request() is only partially supported
+ * - two functions are not implemented yet - they exist but do nothing: xmlrpc_parse_method_descriptions and
+ *   xmlrpc_server_register_introspection_callback
+ * - php arrays indexed with integer keys starting above zero or whose keys are
+ *   not in a strict sequence will be converted into xmlrpc structs, not arrays
+ * - error codes and error strings in Fault responses generated by the Server for invalid calls are different
+ * Possibly to fix:
+ * - xmlrpc_server_create() returns an object instead of a resource
+ * - a single NULL value passed to xmlrpc_encode_request(null, $val) will be decoded as '', not NULL
+ *   (the extension generates an invalid xmlrpc response in this case)
+ * - php arrays indexed with mixed string/integer keys will preserve the integer keys in the generated structs
+ * - server method `system.getCapabilities` returns different results
+ * - server method `system.describeMethods` returns partial data compared to what can be added via
+ *   xmlrpc_parse_method_descriptions and xmlrpc_server_register_introspection_callback - but the native extension
+ *   version of the same method is buggy anyway: it does not list any method's definition...
+ * Won't fix:
+ * - differences in the generated xml
+ *   - the native extension always encodes double values using 13 decimal digits (or 6, depending on version), and pads
+ *     with zeros. We use 13 decimal digits and do not pad. Eg:
+ *     value 1.1 is encoded as <double>1.1</double> instead of <double>1.1000000000000</double>
+ *   - the native extension encodes chars "<", ">" and "&" using numeric entities. We use xml named entities, eg:
+ *     value '&' is encoded as <string>&amp;</string> instead of <string>&#38;</string>.
+ *     Also, we do encode the single quote character "'" as &quot;, whereas the extension does not encode it at all
+ *   - when encoding base64 values, we don't add encoded newline characters (&#10;)
+ *   - some versions of the extension have a bug encoding Latin-1 characters with code points between 200 and 209
+ *     (see https://bugs.php.net/bug.php?id=80559). We do not
+ *   - calling `xmlrpc_encode_request($methodName, $utf8text, options(array('encoding' => 'UTF-8')))` is buggy with
+ *     the extension (wrong character entities are generated). It works with us
+ * - differences in parsing xml
+ *   - some invalid requests / responses will not be accepted that the native extension allows through:
+ *     - missing 'param' inside 'params'
+ *       eg. <methodCall><methodName>hey</methodName><params><value><string>hey</string></value></params></methodCall>
+ * - differences in the API:
+ *   - arrays which look like an xmlrpc fault and are passed to xmlrpc_encode_request() will be encoded
+ *     as structs (the extension generates an invalid xmlrpc request in this case)
+ *   - sending a request for `system.methodHelp` and `system.methodSignature` for a method registered with a Server
+ *     without adding any related introspection data results in an invalid response with the native extension; it does
+ *     not with our code
+ *   - calling `xmlrpc_server_add_introspection_data` with method signatures makes the server validate the number
+ *     and type of incoming parameters in later calls to `xmlrpc_server_call_method`, relieving the developer from
+ *     having to implement the same checks manually in her php functions
+ *   - marking input parameters as optional in the data passed to calls to `xmlrpc_server_add_introspection_data` and
+ *     `xmlrpc_server_register_introspection_callback` will change the number of method signatures displayed by the
+ *     server in responses to calls to `system.methodSignature`.
+ *     Eg. passing in one signature with one optional param will result in two signatures displayed, one with no params
+ *     and one with one param
+ */
+
+
+namespace PhpXmlRpc\Polyfill\XmlRpc;
+
+use PhpXmlRpc\Encoder;
+use PhpXmlRpc\PhpXmlRpc;
+use PhpXmlRpc\Request;
+use PhpXmlRpc\Response;
+use PhpXmlRpc\Server as BaseServer;
+use PhpXmlRpc\Value;
+
+final class XmlRpc
+{
+    public static $xmlpc_double_precision = 13;
+
+    /**
+     * Decode the xml generated by xmlrpc_encode() into native php types
+     * @param string $xml
+     * @param string $encoding target charset encoding for the returned data. Note: when the xml string contains any
+     *                         characters which can not be represented in the target encoding, the returned data will
+     *                         be in utf8
+     * @return mixed
+     */
+    public static function xmlrpc_decode($xml, $encoding = "iso-8859-1")
+    {
+        $encoder = new Encoder();
+        if (strpos($xml, '<methodResponse>') === false) {
+            // strip out unnecessary xml in case we're deserializing a single param.
+            // in case of a complete response, we do not have to strip anything
+            // please note that the test below has LARGE space for improvement (eg. it might trip on xml comments...)
+            $xml = preg_replace(array('!\s*<params>\s*<param>\s*!', '!\s*</param>\s*</params>\s*$!'), array('', ''), $xml);
+        }
+
+        $defaultEncoding = PhpXmlRpc::$xmlrpc_internalencoding;
+        PhpXmlRpc::$xmlrpc_internalencoding = 'UTF-8';
+
+        $options = array('extension_api');
+        if (strtoupper($encoding) != 'UTF-8') {
+            // NB: always set xmlrpc_internalencoding = 'UTF-8' when setting 'extension_api_encoding'
+            $options['extension_api_encoding'] = $encoding;
+        }
+
+        $val = $encoder->decodeXml($xml);
+
+        if (!$val) {
+            $out = null; // instead of false
+        } else {
+            if ($val instanceof Response) {
+                if ($fc = $val->faultCode()) {
+                    $fs = $val->faultString();
+                    $out = array('faultCode' => $fc, 'faultString' => self::fromUtf8($encoding, $fs));
+                } else {
+                    $out = $encoder->decode($val->value(), $options);
+                }
+            } else {
+                $out = $encoder->decode($val, $options);
+            }
+        }
+
+        PhpXmlRpc::$xmlrpc_internalencoding = $defaultEncoding;
+
+        return $out;
+    }
+
+    /**
+     * Decode an xmlrpc request (or response) into native PHP types
+     * @param string $xml
+     * @param string $method (will not be set when decoding responses)
+     * @param string $encoding target charset encoding for the returned data. Note: when the xml string contains any
+     *                         characters which can not be represented in the target encoding, the returned data will
+     *                         be in utf8
+     * @return mixed
+     *
+     * @bug fails for $xml === true, $xml === false, $xml === integer, $xml === float
+     */
+    public static function xmlrpc_decode_request($xml, &$method, $encoding = "iso-8859-1")
+    {
+        $encoder = new Encoder();
+
+        $defaultEncoding = PhpXmlRpc::$xmlrpc_internalencoding;
+        PhpXmlRpc::$xmlrpc_internalencoding = 'UTF-8';
+
+        $options = array('extension_api');
+        if (strtoupper($encoding) != 'UTF-8') {
+            // NB: always set xmlrpc_internalencoding = 'UTF-8' when setting 'extension_api_encoding'
+            $options['extension_api_encoding'] = $encoding;
+        }
+
+        $val = $encoder->decodeXml($xml);
+        if (!$val) {
+            $out = null; // instead of false
+        } else {
+            if ($val instanceof Response) {
+                if ($fc = $val->faultCode()) {
+                    $out = array('faultCode' => $fc, 'faultString' => self::fromUtf8($encoding, $val->faultString()));
+                } else {
+                    $out = $encoder->decode($val->value(), $options);
+                }
+            } else if ($val instanceof Request) {
+                $method = self::fromUtf8($encoding, $val->method());
+                $out = array();
+                $pn = $val->getNumParams();
+                for ($i = 0; $i < $pn; $i++)
+                    $out[] = $encoder->decode($val->getParam($i), $options);
+            } else {
+                /// @todo copy lib behaviour in this case
+                $out = null;
+            }
+        }
+
+        PhpXmlRpc::$xmlrpc_internalencoding = $defaultEncoding;
+
+        return $out;
+    }
+
+    /**
+     * Given a PHP val, convert it to xmlrpc code (wrapped up in either params/param elements or a fault element).
+     * @param mixed $val
+     * @return string
+     * @todo test what happens with arrays with faultCode === 0|''|null
+     */
+    public static function xmlrpc_encode($val)
+    {
+        $encoder = new Encoder();
+
+        $defaultPrecision = PhpXmlRpc::$xmlpc_double_precision;
+        PhpXmlRpc::$xmlpc_double_precision = self::$xmlpc_double_precision;
+        $defaultEncoding = PhpXmlRpc::$xmlrpc_internalencoding;
+        PhpXmlRpc::$xmlrpc_internalencoding = 'ISO-8859-1';
+
+        $eval = $encoder->encode($val, array('extension_api'));
+
+        if (is_array($val) && isset($val['faultCode'])) {
+            $out = "<?xml version=\"1.0\" encoding=\"utf-8\"?" . ">\n<fault>\n " . $eval->serialize('US-ASCII') . "</fault>";
+        } else {
+            $out = "<?xml version=\"1.0\" encoding=\"utf-8\"?" . ">\n<params>\n<param>\n " . $eval->serialize('US-ASCII') . "</param>\n</params>";
+        }
+
+        PhpXmlRpc::$xmlrpc_internalencoding = $defaultEncoding;
+        PhpXmlRpc::$xmlpc_double_precision = $defaultPrecision;
+
+        return $out;
+    }
+
+    /**
+     * Given a method name and array of php values, create an xmlrpc request out
+     * of them. If method name === null, create an xmlrpc response instead
+     * @param string $method
+     * @param array $params
+     * @param array $output_options options array. At the moment only partial support for 'encoding' and 'escaping' is
+     *                              provided.
+     *                              encoding: iso-8859-1, utf-8
+     *                              escaping: [markup], [markup,non-print]. non-ascii is treated the same as non-print
+     * @return string
+     *
+     * @todo complete parsing/usage of options: encoding, escaping.
+     * @todo might, or not, implement support for options: output_type, verbosity
+     */
+    public static function xmlrpc_encode_request($method, $params, $output_options = array())
+    {
+        $encoder = new Encoder();
+
+        $internalEncoding = 'ISO-8859-1';
+        $targetEncoding = 'iso-8859-1';
+        $targetCharset = 'US-ASCII';
+
+        if (isset($output_options['encoding'])) {
+            $targetEncoding = $output_options['encoding'];
+            $internalEncoding = $targetEncoding;
+        }
+
+        if (isset($output_options['escaping'])) {
+            switch(true) {
+                /// @todo improve this:
+                ///       escaping strategies supported by the native extension can be combined, and are:
+                ///         - cdata: wraps text in a cdata section
+                ///         - non-print: uses utf8 char entities for chars <32 and >126
+                ///         - non-ascii: uses utf8 char entities for chars > 127
+                ///         - markup: uses utf8 char entities for & " < >
+                ///       If not specified, it defaults to markup | non-ascii | non-print
+                ///       Otoh the default serialization strategy from phpxmlrpc is to
+                ///         - always escape & " < > and '
+                ///         - when going ut8 -> utf8, touch nothing else
+                ///         - when going iso-8859-1 -> ascii, convert chars <32 and 160-255 (but not 127-159)
+                ///         - when going utf8 -> ascii, convert chars <32 and >= 128 (but not 127)
+                ///         - never wrap the text in a cdata section
+                ///        We should:
+                ///        1. log a warning if being passed options which do not make sense, eg.
+                ///           - cdata along with any other option
+                ///           - any strategy, apart cdata, missing markup (as we always escape markup)
+                ///           - an empty array (same)
+                ///        2. support cdata escaping (done correctly)
+                ///        3. support different escaping for non-print and non-ascii
+                case is_array($output_options['escaping']) && !in_array('non-print', $output_options['escaping']) && !in_array('non-ascii', $output_options['escaping']):
+                case $output_options['escaping'] == 'markup':
+                case $output_options['escaping'] == 'cdata':
+                    $targetCharset = $targetEncoding;
+            }
+        }
+
+        $output_options = array('extension_api');
+
+        $defaultPrecision = PhpXmlRpc::$xmlpc_double_precision;
+        PhpXmlRpc::$xmlpc_double_precision = self::$xmlpc_double_precision;
+        $defaultEncoding = PhpXmlRpc::$xmlrpc_internalencoding;
+        PhpXmlRpc::$xmlrpc_internalencoding = $internalEncoding;
+
+        if ($method !== null) {
+            // mimic EPI behaviour: if ($val === NULL) then send NO parameters
+            if (!is_array($params)) {
+                if ($params === NULL) {
+                    $params = array();
+                } else {
+                    $params = array($params);
+                }
+            } else {
+/// @todo fix corner cases
+                // if given a 'hash' array, encode it as a single param
+                $i = 0;
+                $ok = true;
+                foreach ($params as $key => $value)
+                    if ($key !== $i) {
+                        $ok = false;
+                        break;
+                    } else
+                        $i++;
+                if (!$ok) {
+                    $params = array($params);
+                }
+            }
+
+            $values = array();
+            foreach ($params as $key => $value) {
+                $values[] = $encoder->encode($value, $output_options);
+            }
+
+            // create request
+            $req = new Request($method, $values);
+            $out = preg_replace('!^<\\?xml version="1\\.0" encoding="'.$targetCharset.'" \\?>!', "<?xml version=\"1.0\" encoding=\"$targetEncoding\"?>", $req->serialize($targetCharset));
+        } else {
+            // create response
+            if (is_array($params) && self::xmlrpc_is_fault($params))
+                $resp = new Response(0, (integer)$params['faultCode'], (string)$params['faultString']);
+            else
+                $resp = new Response($encoder->encode($params, $output_options));
+            $out = "<?xml version=\"1.0\" encoding=\"$targetEncoding\"?" . ">\n" . $resp->serialize($targetCharset);
+        }
+
+        PhpXmlRpc::$xmlrpc_internalencoding = $defaultEncoding;
+        PhpXmlRpc::$xmlpc_double_precision = $defaultPrecision;
+
+        return $out;
+    }
+
+    /**
+     * Given a php value, return its corresponding xmlrpc type
+     * @param mixed $value
+     * @return string
+     *
+     * @bug fails compatibility for array('2' => true, false)
+     * @bug fails compatibility for array(true, 'world')
+     */
+    public static function xmlrpc_get_type($value)
+    {
+        switch (strtolower(gettype($value))) {
+            case 'string':
+                return Value::$xmlrpcString;
+            case 'integer':
+            case 'resource':
+                return Value::$xmlrpcInt;
+            case 'double':
+                return Value::$xmlrpcDouble;
+            case 'boolean':
+                return Value::$xmlrpcBoolean;
+            case 'array':
+                $i = 0;
+                $ok = true;
+                foreach ($value as $key => $valueue)
+                    if ($key !== $i) {
+                        $ok = false;
+                        break;
+                    } else
+                        $i++;
+
+                return $ok ? Value::$xmlrpcArray : Value::$xmlrpcStruct;
+            case 'object':
+                if ($value instanceof Value) {
+                    $type = $value->scalarTyp();
+                    return str_replace('dateTime.iso8601', 'datetime', $type);
+                } elseif ($value instanceof \stdClass && isset($value->xmlrpc_type)) {
+                    switch($value->xmlrpc_type) {
+                        case 'datetime':
+                        case 'base64':
+                            return $value->xmlrpc_type;
+                        default:
+                            return 'none';
+
+                    }
+                }
+                return Value::$xmlrpcStruct;
+            case 'null':
+                return Value::$xmlrpcBase64; // go figure why...
+        }
+    }
+
+    /**
+     * Checks if a given php array corresponds to an xmlrpc fault response
+     * @param array $arg
+     * @return boolean
+     */
+    public static function xmlrpc_is_fault($arg)
+    {
+        return is_array($arg) && array_key_exists('faultCode', $arg) && array_key_exists('faultString', $arg);
+    }
+
+    /**
+     * @param string $xml
+     * @return array
+     */
+    public static function xmlrpc_parse_method_descriptions($xml)
+    {
+        return Server::parse_method_descriptions($xml);
+    }
+
+    /** Server side ***************************************************************/
+
+    /**
+     * @param Server $server
+     * @param array $desc
+     * @return int
+     */
+    public static function xmlrpc_server_add_introspection_data($server, $desc)
+    {
+        if ($server instanceof Server) {
+            return $server->add_introspection_data($desc);
+        }
+        return 0;
+    }
+
+    /**
+     * Parses XML request and calls corresponding method
+     * @param Server $server
+     * @param string $xml
+     * @param mixed $user_data
+     * @param array $output_options
+     * @return string
+     */
+    public static function xmlrpc_server_call_method($server, $xml, $user_data, $output_options = array())
+    {
+        $server->user_data = $user_data;
+        return $server->service($xml, true);
+    }
+
+    /**
+     * Create a new xmlrpc server instance
+     * @return Server
+     */
+    public static function xmlrpc_server_create()
+    {
+        $s = new Server();
+        $s->functions_parameters_type = 'epivals';
+        $s->compress_response = false; // since we will not be outputting any http headers to go with it
+        return $s;
+    }
+
+    /**
+     * This function actually does nothing, but it is kept for compatibility.
+     * To destroy a server object, just unset() it, or send it out of scope...
+     * @param Server $server
+     * @return integer
+     */
+    public static function xmlrpc_server_destroy($server)
+    {
+        if ($server instanceof Server)
+            return 1;
+        return 0;
+    }
+
+    /**
+     * @param Server $server
+     * @param string $function
+     * @return bool
+     */
+    public static function xmlrpc_server_register_introspection_callback($server, $function)
+    {
+        if ($server instanceof Server) {
+            return $server->register_introspection_callback($function);
+        }
+        return false;
+    }
+
+    /**
+     * Add a php function as xmlrpc method handler to an existing server.
+     * PHP function sig: f(string $methodname, array $params, mixed $extra_data)
+     * @param Server $server
+     * @param string $method_name
+     * @param string $function
+     * @return boolean true on success or false
+     */
+    public static function xmlrpc_server_register_method($server, $method_name, $function)
+    {
+        if ($server instanceof BaseServer) {
+            $server->add_to_map($method_name, $function);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Set string $val to a known xmlrpc type (base64 or datetime only), for serializing it later
+     * (NB: this will turn the string into an object!).
+     * @param string $val
+     * @param string $type
+     * @return boolean false if conversion did not take place
+     */
+    public static function xmlrpc_set_type(&$val, $type)
+    {
+        if (is_string($val)) {
+            if ($type == 'base64') {
+                $value = array(
+                    'scalar' => $val,
+                    'xmlrpc_type' => 'base64'
+                );
+                $val = (object)$value;
+            } elseif ($type == 'datetime') {
+                if (preg_match('/([0-9]{4}[0-1][0-9][0-3][0-9])T([0-5][0-9]):([0-5][0-9]):([0-5][0-9])/', $val)) {
+                    // add 3 object members to make it more compatible to user code
+                    $value = array(
+                        'scalar' => $val,
+                        'xmlrpc_type' => 'datetime',
+                        'timestamp' => \PhpXmlRpc\Helper\Date::iso8601Decode($val)
+                    );
+                    $val = (object)$value;
+                } else {
+                    return false;
+                }
+            } else {
+                // @todo EPI will NOT raise a warning for good type names, eg. 'boolean', etc...
+                trigger_error("invalid type '$type' passed to xmlrpc_set_type()");
+                return false;
+            }
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    protected static function fromUtf8($to, $str)
+    {
+        if (strtoupper($to) != 'UTF-8') {
+            /// @todo support mbstring as an alternative, as well as plain utf8_decode if none are available and target is latin-1
+            $dstr = @iconv('UTF-8', $to, $str);
+            if ($dstr !== false) {
+                return $dstr;
+            }
+        }
+        return $str;
+    }
+}

+ 80 - 76
includes/lib_remote.php

@@ -21,8 +21,12 @@
  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
  *
  */
-
 require_once(__DIR__ . "/../Crypt/XXTEA.php");
+require_once(__DIR__ . "/phpxmlrpc/Autoloader.php");
+PhpXmlRpc\Autoloader::register();
+require_once(__DIR__ . "/XmlRPC-bootstrap.php");
+
+use PhpXmlRpc\Polyfill\XmlRpc\XmlRpc as p;
 
 // Screen type for servers
 define("OGP_SCREEN_TYPE_HOME","HOME");
@@ -64,7 +68,7 @@ class OGPRemoteLibrary
 			)));
 		
 		$status = @file_get_contents("http://".$this->host.":".$this->port."/RPC2", false, $context);
-		return xmlrpc_decode($status);
+		return p::xmlrpc_decode($status);
 	}
 
 	private function encryptParam($param)
@@ -126,7 +130,7 @@ class OGPRemoteLibrary
 	{
 		$args = $this->encryptParam(trim($file));
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("rfile_exists", $args);
+		$request = p::xmlrpc_encode_request("rfile_exists", $args);
 		$status = $this->sendRequest($request);
 		if ( $status === 0 )
 			return 1;
@@ -143,7 +147,7 @@ class OGPRemoteLibrary
 	{
 		$param = "hello";
 		$args = $this->encryptParam($param);
-		$request = xmlrpc_encode_request("quick_chk", $args);
+		$request = p::xmlrpc_encode_request("quick_chk", $args);
 		$status = $this->sendRequest($request);
 		// If 1 is returned then the encryption key did not match.
 		if ( $status === 1 )
@@ -162,7 +166,7 @@ class OGPRemoteLibrary
 	{
 		$params_array = $this->encrypt_params($screen_type,$home_id,$home_path,$nb_of_lines,$console_log);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request('get_log',$params_array);
+		$request = p::xmlrpc_encode_request('get_log',$params_array);
 		$response = $this->sendRequest($request);
 		if ( $response === NULL )
 			return 0;
@@ -194,7 +198,7 @@ class OGPRemoteLibrary
 		$params_array = $this->encrypt_params($home_id,$server_ip,$server_port,
 			$control_protocol,$control_password,$control_type,$home_path);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("stop_server", $params_array);
+		$request = p::xmlrpc_encode_request("stop_server", $params_array);
 
 		$status = $this->sendRequest($request);
 
@@ -218,7 +222,7 @@ class OGPRemoteLibrary
 		$params_array = $this->encrypt_params($home_id,$server_ip,$server_port,
 			$control_protocol,$control_password,$control_type, $rconCommand);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("send_rcon_command", $params_array);
+		$request = p::xmlrpc_encode_request("send_rcon_command", $params_array);
 		
 		$response = $this->sendRequest($request);
 		
@@ -252,13 +256,13 @@ class OGPRemoteLibrary
 		$args = trim($args);
 		$args = $this->encryptParam($args);
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("readfile", $args);
+		$request = p::xmlrpc_encode_request("readfile", $args);
 		$response = $this->sendRequest($request);
 
 		if ( $response === NULL )
 			return -1;
 
-		if ( is_array($response) && xmlrpc_is_fault($response))
+		if ( is_array($response) && p::xmlrpc_is_fault($response))
 			return -1;
 
 		@list($retval,$data_tmp) = @explode(";",$response);
@@ -281,7 +285,7 @@ class OGPRemoteLibrary
 		$content = base64_encode($content);
 		$params = $this->encrypt_params($writefile,$content);
 		$this->add_enc_chk($params);
-		$request = xmlrpc_encode_request("writefile", $params);
+		$request = p::xmlrpc_encode_request("writefile", $params);
 		$response = $this->sendRequest($request);
 
 		if ( $response === 1 )
@@ -298,7 +302,7 @@ class OGPRemoteLibrary
 		// Must have a parameter even if one is not used.
 		$args = $this->encryptParam("reboot");
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("rebootnow", $args);
+		$request = p::xmlrpc_encode_request("rebootnow", $args);
 		$response = $this->sendRequest($request);
 
 		if ( $response )
@@ -313,7 +317,7 @@ class OGPRemoteLibrary
 	{
 		$params = $this->encrypt_params($home_id,$game_home,$mod,$exec_folder_path,$exec_path,$precmd,$postcmd);
 		$this->add_enc_chk($params);
-		$request = xmlrpc_encode_request("steam", $params);
+		$request = p::xmlrpc_encode_request("steam", $params);
 		$response = $this->sendRequest($request);
 
 		if ( $response === 1 )
@@ -332,7 +336,7 @@ class OGPRemoteLibrary
 	{
 		$params = $this->encrypt_params($home_id,$game_home,$mod,$modname,$betaname,$betapwd,$user,$pass,$guard,$exec_folder_path,$exec_path,$precmd,$postcmd,$cfg_os,$lockFiles,$archBits);
 		$this->add_enc_chk($params);
-		$request = xmlrpc_encode_request("steam_cmd", $params);
+		$request = p::xmlrpc_encode_request("steam_cmd", $params);
 		$response = $this->sendRequest($request);
 
 		if ( $response === 1 )
@@ -349,7 +353,7 @@ class OGPRemoteLibrary
 		$params = $this->encrypt_params($appId, $pureOutput);
 		$this->add_enc_chk($params);
 
-		$request = xmlrpc_encode_request("fetch_steam_version", $params);
+		$request = p::xmlrpc_encode_request("fetch_steam_version", $params);
 		$response = $this->sendRequest($request);
 
 		return $response;
@@ -362,7 +366,7 @@ class OGPRemoteLibrary
 		$params = $this->encrypt_params($game_home, $mod, $pureOutput);
 		$this->add_enc_chk($params);
 
-		$request = xmlrpc_encode_request("installed_steam_version", $params);
+		$request = p::xmlrpc_encode_request("installed_steam_version", $params);
 		$response = $this->sendRequest($request);
 
 		return $response;
@@ -390,7 +394,7 @@ class OGPRemoteLibrary
 						$startup_cmd, $cpu, $nice, $preStart, $envVars, $game_key, $archBits, $console_log);
 
 		$this->add_enc_chk($params);
-		$request = xmlrpc_encode_request("automatic_steam_update", $params);
+		$request = p::xmlrpc_encode_request("automatic_steam_update", $params);
 		$response = $this->sendRequest($request);
 
 		return $response;
@@ -404,7 +408,7 @@ class OGPRemoteLibrary
 	{
 		$params = $this->encrypt_params($home_id,$home_path,$ms_home_id,$ms_home_path,$exec_folder_path,$exec_path,$precmd,$postcmd);
 		$this->add_enc_chk($params);
-		$request = xmlrpc_encode_request("master_server_update", $params);
+		$request = p::xmlrpc_encode_request("master_server_update", $params);
 		$response = $this->sendRequest($request);
 
 		if ( $response === 1 )
@@ -424,7 +428,7 @@ class OGPRemoteLibrary
 	{
 		$params = $this->encrypt_params($game_home, $mod);
 		$this->add_enc_chk($params);
-		$request = xmlrpc_encode_request("game_update_active", $params);
+		$request = p::xmlrpc_encode_request("game_update_active", $params);
 		if(!$response = $this->sendRequest($request) )
 			return -1;
 		else if ( $response === 1 )
@@ -443,13 +447,13 @@ class OGPRemoteLibrary
 	{
 		$params_array = $this->encrypt_params($url,$dest,$filename,$action,$post_script);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("start_file_download",$params_array);
+		$request = p::xmlrpc_encode_request("start_file_download",$params_array);
 		$response = $this->sendRequest($request);
 
 		if( !$response )
 			return -1;
 
-		if (is_array($response) && xmlrpc_is_fault($response))
+		if (is_array($response) && p::xmlrpc_is_fault($response))
 			return -3;
 
 		return $response;
@@ -459,7 +463,7 @@ class OGPRemoteLibrary
 	{
 		$args = $this->encryptParam($pid);
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("is_file_download_in_progress", $args);
+		$request = p::xmlrpc_encode_request("is_file_download_in_progress", $args);
 		return $this->sendRequest($request);
 	}
 
@@ -467,7 +471,7 @@ class OGPRemoteLibrary
 	{
 		$params_array = $this->encrypt_params($file_location,$destination);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("uncompress_file",$params_array);
+		$request = p::xmlrpc_encode_request("uncompress_file",$params_array);
 		return $this->sendRequest($request);
 	}
 
@@ -478,7 +482,7 @@ class OGPRemoteLibrary
 	{
 		$params_array = $this->encrypt_params($home_id,$home_path,$url,$exec_folder_path,$exec_path,$precmd,$postcmd,$filesToLock);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("start_rsync_install",$params_array);
+		$request = p::xmlrpc_encode_request("start_rsync_install",$params_array);
 		$response = $this->sendRequest($request);
 
 		if ( $response === 1 )
@@ -493,13 +497,13 @@ class OGPRemoteLibrary
 	{
 		$args = $this->encryptParam($home);
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("rsync_progress",$args);
+		$request = p::xmlrpc_encode_request("rsync_progress",$args);
 		$response = $this->sendRequest($request);
 
 		if( !$response )
 			return -1;
 
-		#if (is_array($response) && xmlrpc_is_fault($response))
+		#if (is_array($response) && p::xmlrpc_is_fault($response))
 		 # return -3;
 
 		return $response;
@@ -512,10 +516,10 @@ class OGPRemoteLibrary
 	{
 		$args = $this->encryptParam($args);
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("dirlist",$args);
+		$request = p::xmlrpc_encode_request("dirlist",$args);
 		if( !$response = $this->sendRequest($request))
 			return -1;
-		if (is_array($response) && xmlrpc_is_fault($response))
+		if (is_array($response) && p::xmlrpc_is_fault($response))
 			return -3;
 		if( $response < 0 )
 			return -2;
@@ -531,11 +535,11 @@ class OGPRemoteLibrary
 	{
 		$args = $this->encryptParam($args);
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("dirlistfm", $args);
+		$request = p::xmlrpc_encode_request("dirlistfm", $args);
 		$response = $this->sendRequest($request);
 		if ( $response === NULL )
 			return -1;
-		if (is_array($response) && xmlrpc_is_fault($response))
+		if (is_array($response) && p::xmlrpc_is_fault($response))
 			return -3;
 		if( $response < 0 )
 			return -2;
@@ -554,7 +558,7 @@ class OGPRemoteLibrary
 	{
 		$args = NULL;
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("cpu_count", $args);
+		$request = p::xmlrpc_encode_request("cpu_count", $args);
 		$status = $this->sendRequest($request);
 		if ( empty($status) )
 		{
@@ -567,7 +571,7 @@ class OGPRemoteLibrary
 	{
 		$params_array = $this->encrypt_params($home_id, $nice);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("renice_process",$params_array);
+		$request = p::xmlrpc_encode_request("renice_process",$params_array);
 		return $this->sendRequest($response);
 	}
 
@@ -582,12 +586,12 @@ class OGPRemoteLibrary
 		$params_array = $this->encrypt_params($home_id, $game_home, $game_binary,
 			$run_dir, $startup_cmd, $server_port, $server_ip, $cpu, $nice, $preStart, $envVars, $game_key, $console_log);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("universal_start", $params_array);
+		$request = p::xmlrpc_encode_request("universal_start", $params_array);
 		$response = $this->sendRequest($request);
 		if($response === NULL)
 			return -1;
 
-		if (is_array($response) && xmlrpc_is_fault($response))
+		if (is_array($response) && p::xmlrpc_is_fault($response))
 			return -2;
 
 		return $response;
@@ -597,12 +601,12 @@ class OGPRemoteLibrary
 	{
 		$params_array = $this->encrypt_params($game_home, $filesToLockUnlock, $action);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("lock_additional_files", $params_array);
+		$request = p::xmlrpc_encode_request("lock_additional_files", $params_array);
 		$response = $this->sendRequest($request);
 		if($response === NULL)
 			return -1;
 
-		if (is_array($response) && xmlrpc_is_fault($response))
+		if (is_array($response) && p::xmlrpc_is_fault($response))
 			return -2;
 
 		return $response;
@@ -613,7 +617,7 @@ class OGPRemoteLibrary
 	{
 		$args = NULL;
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("what_os", $args);
+		$request = p::xmlrpc_encode_request("what_os", $args);
 		$status = $this->sendRequest($request);
 		return "$status";
 	}
@@ -626,7 +630,7 @@ class OGPRemoteLibrary
 		$args = "chk";
 		$args = $this->encryptParam($args);
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("discover_ips", $args);
+		$request = p::xmlrpc_encode_request("discover_ips", $args);
 		$status = $this->sendRequest($request);
 
 		if ( $status == 0 )
@@ -643,7 +647,7 @@ class OGPRemoteLibrary
 	{
 		$params = $this->encrypt_params($screen_type,$home_id);
 		$this->add_enc_chk($params);
-		$request = xmlrpc_encode_request("is_screen_running", $params);
+		$request = p::xmlrpc_encode_request("is_screen_running", $params);
 		$status = $this->sendRequest($request);
 		if ( $status === 1 )
 			return 1;
@@ -657,7 +661,7 @@ class OGPRemoteLibrary
 	{
 		$args = $this->encrypt_params("mon_stats");
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("mon_stats", $args);
+		$request = p::xmlrpc_encode_request("mon_stats", $args);
 		$response = $this->sendRequest($request);
 
 		@list($retval,$data_tmp) = @explode(";",$response);
@@ -685,7 +689,7 @@ class OGPRemoteLibrary
 	{
 		$params_array = $this->encrypt_params($source_home, $dest_home, $owner);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("clone_home", $params_array);
+		$request = p::xmlrpc_encode_request("clone_home", $params_array);
 		$status = $this->sendRequest($request);
 
 		// Copy was successful.
@@ -708,7 +712,7 @@ class OGPRemoteLibrary
 	{
 		$args = $this->encryptParam($game_home_del);
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("remove_home", $args);
+		$request = p::xmlrpc_encode_request("remove_home", $args);
 		$status = $this->sendRequest($request);
 
 		// Delete was successful.
@@ -730,7 +734,7 @@ class OGPRemoteLibrary
 			$control_protocol,$control_password,$control_type,
 			$home_path,$server_exe,$run_dir,$cmd,$cpu,$nice,$preStart,$envVars, $game_key, $console_log);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("restart_server", $params_array);
+		$request = p::xmlrpc_encode_request("restart_server", $params_array);
 
 		$status = $this->sendRequest($request);
 
@@ -752,7 +756,7 @@ class OGPRemoteLibrary
 	{
 		$args = $this->encryptParam($command);
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("sudo_exec", $args);
+		$request = p::xmlrpc_encode_request("sudo_exec", $args);
 
 		$status = $this->sendRequest($request);
 
@@ -776,7 +780,7 @@ class OGPRemoteLibrary
 	{
 		$args = $this->encryptParam($command);
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("exec", $args);
+		$request = p::xmlrpc_encode_request("exec", $args);
 		$response = $this->sendRequest($request);
 
 		@list($retval,$data_tmp) = @explode(";",$response);
@@ -798,7 +802,7 @@ class OGPRemoteLibrary
 	{
 		$params_array = $this->encrypt_params($action, $path);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("secure_path", $params_array);
+		$request = p::xmlrpc_encode_request("secure_path", $params_array);
 
 		$status = $this->sendRequest($request);
 
@@ -821,7 +825,7 @@ class OGPRemoteLibrary
 	{
 		$args = $this->encryptParam($path);
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("get_chattr", $args);
+		$request = p::xmlrpc_encode_request("get_chattr", $args);
 		$status = $this->sendRequest($request);
 
 		@list($retval,$data_tmp) = @explode(";",$status);
@@ -843,7 +847,7 @@ class OGPRemoteLibrary
 	{
 		$params_array = $this->encrypt_params($action, $login, $password, $home_path);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("ftp_mgr", $params_array);
+		$request = p::xmlrpc_encode_request("ftp_mgr", $params_array);
 		$status = $this->sendRequest($request);
 		@list($retval,$data_tmp) = @explode(";",$status);
 
@@ -867,7 +871,7 @@ class OGPRemoteLibrary
 	{
 		$params_array = $this->encrypt_params($files,$destination,$archive_name,$archive_type);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("compress_files",$params_array);
+		$request = p::xmlrpc_encode_request("compress_files",$params_array);
 		return $this->sendRequest($request);
 	}
 	
@@ -875,7 +879,7 @@ class OGPRemoteLibrary
 	{
 		$args = NULL;
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("stop_fastdl",$args);
+		$request = p::xmlrpc_encode_request("stop_fastdl",$args);
 		return $this->sendRequest($request);
 	}
 	
@@ -883,7 +887,7 @@ class OGPRemoteLibrary
 	{
 		$args = NULL;
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("start_fastdl",$args);
+		$request = p::xmlrpc_encode_request("start_fastdl",$args);
 		return $this->sendRequest($request);
 	}
 	
@@ -891,7 +895,7 @@ class OGPRemoteLibrary
 	{
 		$args = NULL;
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("restart_fastdl",$args);
+		$request = p::xmlrpc_encode_request("restart_fastdl",$args);
 		return $this->sendRequest($request);
 	}
 	
@@ -899,7 +903,7 @@ class OGPRemoteLibrary
 	{
 		$args = NULL;
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("fastdl_status",$args);
+		$request = p::xmlrpc_encode_request("fastdl_status",$args);
 		$response = $this->sendRequest($request);
 		if($response === -1 or $response === 0)
 			return -1;
@@ -910,7 +914,7 @@ class OGPRemoteLibrary
 	{
 		$args = NULL;
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("fastdl_get_aliases",$args);
+		$request = p::xmlrpc_encode_request("fastdl_get_aliases",$args);
 		$response = $this->sendRequest($request);
 		if(!is_array($response) or count($response) == 0)
 			return -1;
@@ -921,7 +925,7 @@ class OGPRemoteLibrary
 	{
 		$params_array = $this->encrypt_params($alias,$home,$match_file_extension,$match_client_ip);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("fastdl_add_alias",$params_array);
+		$request = p::xmlrpc_encode_request("fastdl_add_alias",$params_array);
 		return $this->sendRequest($request);
 	}
 	
@@ -939,7 +943,7 @@ class OGPRemoteLibrary
 			$params_array = array(0 => $this->encryptParam($aliases));
 
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("fastdl_del_alias",$params_array);
+		$request = p::xmlrpc_encode_request("fastdl_del_alias",$params_array);
 		return $this->sendRequest($request);
 	}
 	
@@ -947,7 +951,7 @@ class OGPRemoteLibrary
 	{
 		$args = NULL;
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("fastdl_get_info",$args);
+		$request = p::xmlrpc_encode_request("fastdl_get_info",$args);
 		$response = $this->sendRequest($request);
 		if($response === -1 or $response == 0)
 			return -1;
@@ -958,7 +962,7 @@ class OGPRemoteLibrary
 	{
 		$params_array = $this->encrypt_params($fd_address, $fd_port, $listing, $autostart_on_agent_startup);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("fastdl_create_config",$params_array);
+		$request = p::xmlrpc_encode_request("fastdl_create_config",$params_array);
 		return $this->sendRequest($request);
 	}
 	
@@ -966,7 +970,7 @@ class OGPRemoteLibrary
 	{
 		$args = $this->encryptParam('restart');
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("agent_restart", $args);
+		$request = p::xmlrpc_encode_request("agent_restart", $args);
 		$response = $this->sendRequest($request);
 		if($response === -1)
 			return -1;
@@ -977,7 +981,7 @@ class OGPRemoteLibrary
 	{
 		$args = NULL;
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("scheduler_list_tasks", $args);
+		$request = p::xmlrpc_encode_request("scheduler_list_tasks", $args);
 		$response = $this->sendRequest($request);
 		if($response === -1 or $response == 0)
 			return -1;
@@ -998,7 +1002,7 @@ class OGPRemoteLibrary
 	{
 		$args = $this->encryptParam($id);
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("scheduler_del_task",$args);
+		$request = p::xmlrpc_encode_request("scheduler_del_task",$args);
 		return $this->sendRequest($request);
 	}
 	
@@ -1006,7 +1010,7 @@ class OGPRemoteLibrary
 	{
 		$args = $this->encryptParam($job);
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("scheduler_add_task",$args);
+		$request = p::xmlrpc_encode_request("scheduler_add_task",$args);
 		return $this->sendRequest($request);
 	}
 	
@@ -1014,7 +1018,7 @@ class OGPRemoteLibrary
 	{
 		$params_array = $this->encrypt_params($job_id, $job);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("scheduler_edit_task",$params_array);
+		$request = p::xmlrpc_encode_request("scheduler_edit_task",$params_array);
 		return $this->sendRequest($request);
 	}
 	
@@ -1022,13 +1026,13 @@ class OGPRemoteLibrary
 	{
 		$params_array = $this->encrypt_params($file, $offset);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("get_file_part",$params_array);
+		$request = p::xmlrpc_encode_request("get_file_part",$params_array);
 		$response = $this->sendRequest($request);
 		
 		if ( $response === NULL )
 			return -1;
 
-		if ( is_array($response) && xmlrpc_is_fault($response))
+		if ( is_array($response) && p::xmlrpc_is_fault($response))
 			return -1;
 		
 		if ( $response === -1 )
@@ -1043,10 +1047,10 @@ class OGPRemoteLibrary
 	{
 		$params_array = $this->encrypt_params($action, $arguments);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("shell_action", $params_array);
+		$request = p::xmlrpc_encode_request("shell_action", $params_array);
 		$response = $this->sendRequest($request);
 		
-		if (is_array($response) && xmlrpc_is_fault($response))
+		if (is_array($response) && p::xmlrpc_is_fault($response))
 			return NULL;
 		
 		$data = NULL;
@@ -1078,13 +1082,13 @@ class OGPRemoteLibrary
 	{
 		$args = $this->encryptParam($home_id);
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("stop_update", $args);
+		$request = p::xmlrpc_encode_request("stop_update", $args);
 		$response = $this->sendRequest($request);
 		
 		if ($response === NULL)
 			return -1;
 		
-		if (is_array($response) && xmlrpc_is_fault($response))
+		if (is_array($response) && p::xmlrpc_is_fault($response))
 			return -1;
 		
 		if ($response === 1)
@@ -1097,9 +1101,9 @@ class OGPRemoteLibrary
 	{
 		$params_array = $this->encrypt_params($protocol, $game_type, $ip, $c_port, $q_port, $s_port);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("remote_query", $params_array);
+		$request = p::xmlrpc_encode_request("remote_query", $params_array);
 		$response = $this->sendRequest($request);
-		if (is_array($response) && xmlrpc_is_fault($response))
+		if (is_array($response) && p::xmlrpc_is_fault($response))
 			return NULL;
 		if ($response === -1 or $response === 0)
 			return NULL;
@@ -1110,13 +1114,13 @@ class OGPRemoteLibrary
 	{
 		$params_array = $this->encrypt_params($home_id, $sgc);
 		$this->add_enc_chk($params_array);
-		$request = xmlrpc_encode_request("send_steam_guard_code", $params_array);
+		$request = p::xmlrpc_encode_request("send_steam_guard_code", $params_array);
 		$response = $this->sendRequest($request);
 		
 		if ($response === NULL)
 			return -1;
 		
-		if (is_array($response) && xmlrpc_is_fault($response))
+		if (is_array($response) && p::xmlrpc_is_fault($response))
 			return -1;
 		
 		if ($response === 1)
@@ -1147,14 +1151,14 @@ class OGPRemoteLibrary
 										$anonymous_login, $user, $pass,
 										$download_method, $url_list, $filename_list);
 		$this->add_enc_chk($params);
-		$request = xmlrpc_encode_request("steam_workshop", $params);
+		$request = p::xmlrpc_encode_request("steam_workshop", $params);
 		$response = $this->sendRequest($request);
 		
 		// Connection Error
 		if ($response === NULL)
 			return -3;
 		// Subroutine Failure
-		if(is_array($response) && xmlrpc_is_fault($response))
+		if(is_array($response) && p::xmlrpc_is_fault($response))
 			return -2;
 		// Error unmet condition
 		if ($response === -1)
@@ -1170,14 +1174,14 @@ class OGPRemoteLibrary
 	{
 		$args = $this->encryptParam("mods_info");
 		$this->add_enc_chk($args);
-		$request = xmlrpc_encode_request("get_workshop_mods_info", $args);
+		$request = p::xmlrpc_encode_request("get_workshop_mods_info", $args);
 		$response = $this->sendRequest($request);
 		$data = array();
 		// Offline
 		if ($response === NULL)
 			return -3;
 		// Failure
-		if(is_array($response) && xmlrpc_is_fault($response))
+		if(is_array($response) && p::xmlrpc_is_fault($response))
 			return -2;
 		// mods directory does not exists
 		if($response === -1)

+ 36 - 0
includes/phpxmlrpc/Autoloader.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace PhpXmlRpc;
+
+/**
+ * In the unlikely event that you are not using Composer to manage class autoloading, here's an autoloader for this lib.
+ * For usage, see any file in the demo/client directory
+ */
+class Autoloader
+{
+    /**
+     * Registers PhpXmlRpc\Autoloader as an SPL autoloader.
+     *
+     * @param bool $prepend Whether to prepend the autoloader or not.
+     */
+    public static function register($prepend = false)
+    {
+        spl_autoload_register(array(__CLASS__, 'autoload'), true, $prepend);
+    }
+
+    /**
+     * Handles autoloading of classes.
+     *
+     * @param string $class A class name.
+     */
+    public static function autoload($class)
+    {
+        if (0 !== strpos($class, 'PhpXmlRpc\\')) {
+            return;
+        }
+
+        if (is_file($file = __DIR__ . str_replace(array('PhpXmlRpc\\', '\\'), '/', $class).'.php')) {
+            require $file;
+        }
+    }
+}

+ 2058 - 0
includes/phpxmlrpc/Client.php

@@ -0,0 +1,2058 @@
+<?php
+
+namespace PhpXmlRpc;
+
+use PhpXmlRpc\Exception\ValueErrorException;
+use PhpXmlRpc\Helper\XMLParser;
+use PhpXmlRpc\Traits\CharsetEncoderAware;
+use PhpXmlRpc\Traits\DeprecationLogger;
+
+/**
+ * Used to represent a client of an XML-RPC server.
+ *
+ * @property int $errno deprecated - public access left in purely for BC.
+ * @property string $errstr deprecated - public access left in purely for BC.
+ * @property string $method deprecated - public access left in purely for BC. Access via getUrl()/__construct()
+ * @property string $server deprecated - public access left in purely for BC. Access via getUrl()/__construct()
+ * @property int $port deprecated - public access left in purely for BC. Access via getUrl()/__construct()
+ * @property string $path deprecated - public access left in purely for BC. Access via getUrl()/__construct()
+ */
+class Client
+{
+    use DeprecationLogger;
+    //use CharsetEncoderAware;
+
+    const USE_CURL_NEVER = 0;
+    const USE_CURL_ALWAYS = 1;
+    const USE_CURL_AUTO = 2;
+
+    const OPT_ACCEPTED_CHARSET_ENCODINGS = 'accepted_charset_encodings';
+    const OPT_ACCEPTED_COMPRESSION = 'accepted_compression';
+    const OPT_AUTH_TYPE = 'authtype';
+    const OPT_CA_CERT = 'cacert';
+    const OPT_CA_CERT_DIR = 'cacertdir';
+    const OPT_CERT = 'cert';
+    const OPT_CERT_PASS = 'certpass';
+    const OPT_COOKIES = 'cookies';
+    const OPT_DEBUG = 'debug';
+    const OPT_EXTRA_CURL_OPTS = 'extracurlopts';
+    const OPT_EXTRA_SOCKET_OPTS = 'extrasockopts';
+    const OPT_KEEPALIVE = 'keepalive';
+    const OPT_KEY = 'key';
+    const OPT_KEY_PASS = 'keypass';
+    const OPT_NO_MULTICALL = 'no_multicall';
+    const OPT_PASSWORD = 'password';
+    const OPT_PROXY = 'proxy';
+    const OPT_PROXY_AUTH_TYPE = 'proxy_authtype';
+    const OPT_PROXY_PASS = 'proxy_pass';
+    const OPT_PROXY_PORT = 'proxyport';
+    const OPT_PROXY_USER = 'proxy_user';
+    const OPT_REQUEST_CHARSET_ENCODING = 'request_charset_encoding';
+    const OPT_REQUEST_COMPRESSION = 'request_compression';
+    const OPT_RETURN_TYPE = 'return_type';
+    const OPT_SSL_VERSION = 'sslversion';
+    const OPT_TIMEOUT = 'timeout';
+    const OPT_USERNAME = 'username';
+    const OPT_USER_AGENT = 'user_agent';
+    const OPT_USE_CURL = 'use_curl';
+    const OPT_VERIFY_HOST = 'verifyhost';
+    const OPT_VERIFY_PEER = 'verifypeer';
+
+    /** @var string */
+    protected static $requestClass = '\\PhpXmlRpc\\Request';
+    /** @var string */
+    protected static $responseClass = '\\PhpXmlRpc\\Response';
+
+    /**
+     * @var int
+     * @deprecated will be removed in the future
+     */
+    protected $errno;
+    /**
+     * @var string
+     * @deprecated will be removed in the future
+     */
+    protected $errstr;
+
+    /// @todo: do all the ones below need to be public?
+
+    /**
+     * @var string
+     */
+    protected $method = 'http';
+    /**
+     * @var string
+     */
+    protected $server;
+    /**
+     * @var int
+     */
+    protected $port = 0;
+    /**
+     * @var string
+     */
+    protected $path;
+
+    /**
+     * @var int
+     */
+    protected $debug = 0;
+    /**
+     * @var string
+     */
+    protected $username = '';
+    /**
+     * @var string
+     */
+    protected $password = '';
+    /**
+     * @var int
+     */
+    protected $authtype = 1;
+    /**
+     * @var string
+     */
+    protected $cert = '';
+    /**
+     * @var string
+     */
+    protected $certpass = '';
+    /**
+     * @var string
+     */
+    protected $cacert = '';
+    /**
+     * @var string
+     */
+    protected $cacertdir = '';
+    /**
+     * @var string
+     */
+    protected $key = '';
+    /**
+     * @var string
+     */
+    protected $keypass = '';
+    /**
+     * @var bool
+     */
+    protected $verifypeer = true;
+    /**
+     * @var int
+     */
+    protected $verifyhost = 2;
+    /**
+     * @var int
+     */
+    protected $sslversion = 0; // corresponds to CURL_SSLVERSION_DEFAULT. Other  CURL_SSLVERSION_ values are supported
+    /**
+     * @var string
+     */
+    protected $proxy = '';
+    /**
+     * @var int
+     */
+    protected $proxyport = 0;
+    /**
+     * @var string
+     */
+    protected $proxy_user = '';
+    /**
+     * @var string
+     */
+    protected $proxy_pass = '';
+    /**
+     * @var int
+     */
+    protected $proxy_authtype = 1;
+    /**
+     * @var array
+     */
+    protected $cookies = array();
+    /**
+     * @var array
+     */
+    protected $extrasockopts = array();
+    /**
+     * @var array
+     */
+    protected $extracurlopts = array();
+    /**
+     * @var int
+     */
+    protected $timeout = 0;
+    /**
+     * @var int
+     */
+    protected $use_curl = self::USE_CURL_AUTO;
+    /**
+     * @var bool
+     *
+     * This determines whether the multicall() method will try to take advantage of the system.multicall xml-rpc method
+     * to dispatch to the server an array of requests in a single http roundtrip or simply execute many consecutive http
+     * calls. Defaults to FALSE, but it will be enabled automatically on the first failure of execution of
+     * system.multicall.
+     */
+    protected $no_multicall = false;
+    /**
+     * @var array
+     *
+     * List of http compression methods accepted by the client for responses.
+     * NB: PHP supports deflate, gzip compressions out of the box if compiled w. zlib.
+     *
+     * NNB: you can set it to any non-empty array for HTTP11 and HTTPS, since in those cases it will be up to CURL to
+     * decide the compression methods it supports. You might check for the presence of 'zlib' in the output of
+     * curl_version() to determine whether compression is supported or not
+     */
+    protected $accepted_compression = array();
+    /**
+     * @var string|null
+     *
+     * Name of compression scheme to be used for sending requests.
+     * Either null, 'gzip' or 'deflate'.
+     */
+    protected $request_compression = '';
+    /**
+     * @var bool
+     *
+     * Whether to use persistent connections for http 1.1 and https. Value set at constructor time.
+     */
+    protected $keepalive = false;
+    /**
+     * @var string[]
+     *
+     * Charset encodings that can be decoded without problems by the client. Value set at constructor time
+     */
+    protected $accepted_charset_encodings = array();
+    /**
+     * @var string
+     *
+     * The charset encoding that will be used for serializing request sent by the client.
+     * It defaults to NULL, which means using US-ASCII and encoding all characters outside the ASCII printable range
+     * using their xml character entity representation (this has the benefit that line end characters will not be mangled
+     * in the transfer, a CR-LF will be preserved as well as a singe LF).
+     * Valid values are 'US-ASCII', 'UTF-8' and 'ISO-8859-1'.
+     * For the fastest mode of operation, set your both your app internal encoding and this to UTF-8.
+     */
+    protected $request_charset_encoding = '';
+    /**
+     * @var string
+     *
+     * Decides the content of Response objects returned by calls to send() and multicall().
+     * Valid values are 'xmlrpcvals', 'phpvals' or 'xml'.
+     *
+     * Determines whether the value returned inside a Response object as results of calls to the send() and multicall()
+     * methods will be a Value object, a plain php value or a raw xml string.
+     * Allowed values are 'xmlrpcvals' (the default), 'phpvals' and 'xml'.
+     * To allow the user to differentiate between a correct and a faulty response, fault responses will be returned as
+     * Response objects in any case.
+     * Note that the 'phpvals' setting will yield faster execution times, but some of the information from the original
+     * response will be lost. It will be e.g. impossible to tell whether a particular php string value was sent by the
+     * server as an xml-rpc string or base64 value.
+     */
+    protected $return_type = XMLParser::RETURN_XMLRPCVALS;
+    /**
+     * @var string
+     *
+     * Sent to servers in http headers. Value set at constructor time.
+     */
+    protected $user_agent;
+
+    /**
+     * CURL handle: used for keep-alive
+     * @internal
+     */
+    public $xmlrpc_curl_handle = null;
+
+    /**
+     * @var array
+     */
+    protected static $options = array(
+        self::OPT_ACCEPTED_CHARSET_ENCODINGS,
+        self::OPT_ACCEPTED_COMPRESSION,
+        self::OPT_AUTH_TYPE,
+        self::OPT_CA_CERT,
+        self::OPT_CA_CERT_DIR,
+        self::OPT_CERT,
+        self::OPT_CERT_PASS,
+        self::OPT_COOKIES,
+        self::OPT_DEBUG,
+        self::OPT_EXTRA_CURL_OPTS,
+        self::OPT_EXTRA_SOCKET_OPTS,
+        self::OPT_KEEPALIVE,
+        self::OPT_KEY,
+        self::OPT_KEY_PASS,
+        self::OPT_NO_MULTICALL,
+        self::OPT_PASSWORD,
+        self::OPT_PROXY,
+        self::OPT_PROXY_AUTH_TYPE,
+        self::OPT_PROXY_PASS,
+        self::OPT_PROXY_USER,
+        self::OPT_PROXY_PORT,
+        self::OPT_REQUEST_CHARSET_ENCODING,
+        self::OPT_REQUEST_COMPRESSION,
+        self::OPT_RETURN_TYPE,
+        self::OPT_SSL_VERSION,
+        self::OPT_TIMEOUT,
+        self::OPT_USE_CURL,
+        self::OPT_USER_AGENT,
+        self::OPT_USERNAME,
+        self::OPT_VERIFY_HOST,
+        self::OPT_VERIFY_PEER,
+    );
+
+    /**
+     * @param string $path either the PATH part of the xml-rpc server URL, or complete server URL (in which case you
+     *                     should use an empty string for all other parameters)
+     *                     e.g. /xmlrpc/server.php
+     *                     e.g. http://phpxmlrpc.sourceforge.net/server.php
+     *                     e.g. https://james:[email protected]:444/xmlrpcserver?agent=007
+     *                     e.g. h2://fast-and-secure-services.org/endpoint
+     * @param string $server the server name / ip address
+     * @param integer $port the port the server is listening on, when omitted defaults to 80 or 443 depending on
+     *                      protocol used
+     * @param string $method the http protocol variant: defaults to 'http'; 'https', 'http11', 'h2' and 'h2c' can
+     *                       be used if CURL is installed. The value set here can be overridden in any call to $this->send().
+     *                       Use 'h2' to make the lib attempt to use http/2 over a secure connection, and 'h2c'
+     *                       for http/2 without tls. Note that 'h2c' will not use the h2c 'upgrade' method, and be
+     *                       thus incompatible with any server/proxy not supporting http/2. This is because POST
+     *                       request are not compatible with h2c upgrade.
+     */
+    public function __construct($path, $server = '', $port = '', $method = '')
+    {
+        // allow user to specify all params in $path
+        if ($server == '' && $port == '' && $method == '') {
+            $parts = parse_url($path);
+            $server = $parts['host'];
+            $path = isset($parts['path']) ? $parts['path'] : '';
+            if (isset($parts['query'])) {
+                $path .= '?' . $parts['query'];
+            }
+            if (isset($parts['fragment'])) {
+                $path .= '#' . $parts['fragment'];
+            }
+            if (isset($parts['port'])) {
+                $port = $parts['port'];
+            }
+            if (isset($parts['scheme'])) {
+                $method = $parts['scheme'];
+            }
+            if (isset($parts['user'])) {
+                $this->username = $parts['user'];
+            }
+            if (isset($parts['pass'])) {
+                $this->password = $parts['pass'];
+            }
+        }
+        if ($path == '' || $path[0] != '/') {
+            $this->path = '/' . $path;
+        } else {
+            $this->path = $path;
+        }
+        $this->server = $server;
+        if ($port != '') {
+            $this->port = $port;
+        }
+        if ($method != '') {
+            $this->method = $method;
+        }
+
+        // if ZLIB is enabled, let the client by default accept compressed responses
+        if (function_exists('gzinflate') || (
+                function_exists('curl_version') && (($info = curl_version()) &&
+                    ((is_string($info) && strpos($info, 'zlib') !== null) || isset($info['libz_version'])))
+            )
+        ) {
+            $this->accepted_compression = array('gzip', 'deflate');
+        }
+
+        // keepalives: enabled by default
+        $this->keepalive = true;
+
+        // by default the xml parser can support these 3 charset encodings
+        $this->accepted_charset_encodings = array('UTF-8', 'ISO-8859-1', 'US-ASCII');
+
+        // NB: this is disabled to avoid making all the requests sent huge... mbstring supports more than 80 charsets!
+        //$ch = $this->getCharsetEncoder();
+        //$this->accepted_charset_encodings = $ch->knownCharsets();
+
+        // initialize user_agent string
+        $this->user_agent = PhpXmlRpc::$xmlrpcName . ' ' . PhpXmlRpc::$xmlrpcVersion;
+    }
+
+    /**
+     * @param string $name see all the OPT_ constants
+     * @param mixed $value
+     * @return $this
+     * @throws ValueErrorException on unsupported option
+     */
+    public function setOption($name, $value)
+    {
+        if (in_array($name, static::$options)) {
+            $this->$name = $value;
+            return $this;
+        }
+
+        throw new ValueErrorException("Unsupported option '$name'");
+    }
+
+    /**
+     * @param string $name see all the OPT_ constants
+     * @return mixed
+     * @throws ValueErrorException on unsupported option
+     */
+    public function getOption($name)
+    {
+        if (in_array($name, static::$options)) {
+            return $this->$name;
+        }
+
+        throw new ValueErrorException("Unsupported option '$name'");
+    }
+
+    /**
+     * Returns the complete list of Client options, with their value.
+     * @return array
+     */
+    public function getOptions()
+    {
+        $values = array();
+        foreach (static::$options as $opt) {
+            $values[$opt] = $this->getOption($opt);
+        }
+        return $values;
+    }
+
+    /**
+     * @param array $options key: any valid option (see all the OPT_ constants)
+     * @return $this
+     * @throws ValueErrorException on unsupported option
+     */
+    public function setOptions($options)
+    {
+        foreach ($options as $name => $value) {
+            $this->setOption($name, $value);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Enable/disable the echoing to screen of the xml-rpc responses received. The default is not to output anything.
+     *
+     * The debugging information at level 1 includes the raw data returned from the XML-RPC server it was querying
+     * (including bot HTTP headers and the full XML payload), and the PHP value the client attempts to create to
+     * represent the value returned by the server.
+     * At level 2, the complete payload of the xml-rpc request is also printed, before being sent to the server.
+     * At level -1, the Response objects returned by send() calls will not carry information about the http response's
+     * cookies, headers and body, which might save some memory
+     *
+     * This option can be very useful when debugging servers as it allows you to see exactly what the client sends and
+     * the server returns. Never leave it enabled for production!
+     *
+     * @param integer $level values -1, 0, 1 and 2 are supported
+     * @return $this
+     */
+    public function setDebug($level)
+    {
+        $this->debug = $level;
+        return $this;
+    }
+
+    /**
+     * Sets the username and password for authorizing the client to the server.
+     *
+     * With the default (HTTP) transport, this information is used for HTTP Basic authorization.
+     * Note that username and password can also be set using the class constructor.
+     * With HTTP 1.1 and HTTPS transport, NTLM and Digest authentication protocols are also supported. To enable them use
+     * the constants CURLAUTH_DIGEST and CURLAUTH_NTLM as values for the auth type parameter.
+     *
+     * @param string $user username
+     * @param string $password password
+     * @param integer $authType auth type. See curl_setopt man page for supported auth types. Defaults to CURLAUTH_BASIC
+     *                          (basic auth). Note that auth types NTLM and Digest will only work if the Curl php
+     *                          extension is enabled.
+     * @return $this
+     */
+    public function setCredentials($user, $password, $authType = 1)
+    {
+        $this->username = $user;
+        $this->password = $password;
+        $this->authtype = $authType;
+        return $this;
+    }
+
+    /**
+     * Set the optional certificate and passphrase used in SSL-enabled communication with a remote server.
+     *
+     * Note: to retrieve information about the client certificate on the server side, you will need to look into the
+     * environment variables which are set up by the webserver. Different webservers will typically set up different
+     * variables.
+     *
+     * @param string $cert the name of a file containing a PEM formatted certificate
+     * @param string $certPass the password required to use it
+     * @return $this
+     */
+    public function setCertificate($cert, $certPass = '')
+    {
+        $this->cert = $cert;
+        $this->certpass = $certPass;
+        return $this;
+    }
+
+    /**
+     * Add a CA certificate to verify server with in SSL-enabled communication when SetSSLVerifypeer has been set to TRUE.
+     *
+     * See the php manual page about CURLOPT_CAINFO for more details.
+     *
+     * @param string $caCert certificate file name (or dir holding certificates)
+     * @param bool $isDir set to true to indicate cacert is a dir. defaults to false
+     * @return $this
+     */
+    public function setCaCertificate($caCert, $isDir = false)
+    {
+        if ($isDir) {
+            $this->cacertdir = $caCert;
+        } else {
+            $this->cacert = $caCert;
+        }
+        return $this;
+    }
+
+    /**
+     * Set attributes for SSL communication: private SSL key.
+     *
+     * NB: does not work in older php/curl installs.
+     * Thanks to Daniel Convissor.
+     *
+     * @param string $key The name of a file containing a private SSL key
+     * @param string $keyPass The secret password needed to use the private SSL key
+     * @return $this
+     */
+    public function setKey($key, $keyPass)
+    {
+        $this->key = $key;
+        $this->keypass = $keyPass;
+        return $this;
+    }
+
+    /**
+     * Set attributes for SSL communication: verify the remote host's SSL certificate, and cause the connection to fail
+     * if the cert verification fails.
+     *
+     * By default, verification is enabled.
+     * To specify custom SSL certificates to validate the server with, use the setCaCertificate method.
+     *
+     * @param bool $i enable/disable verification of peer certificate
+     * @return $this
+     * @deprecated use setOption
+     */
+    public function setSSLVerifyPeer($i)
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        $this->verifypeer = $i;
+        return $this;
+    }
+
+    /**
+     * Set attributes for SSL communication: verify the remote host's SSL certificate's common name (CN).
+     *
+     * Note that support for value 1 has been removed in cURL 7.28.1
+     *
+     * @param int $i Set to 1 to only the existence of a CN, not that it matches
+     * @return $this
+     * @deprecated use setOption
+     */
+    public function setSSLVerifyHost($i)
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        $this->verifyhost = $i;
+        return $this;
+    }
+
+    /**
+     * Set attributes for SSL communication: SSL version to use. Best left at 0 (default value): let cURL decide
+     *
+     * @param int $i see  CURL_SSLVERSION_ constants
+     * @return $this
+     * @deprecated use setOption
+     */
+    public function setSSLVersion($i)
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        $this->sslversion = $i;
+        return $this;
+    }
+
+    /**
+     * Set proxy info.
+     *
+     * NB: CURL versions before 7.11.10 cannot use a proxy to communicate with https servers.
+     *
+     * @param string $proxyHost
+     * @param string $proxyPort Defaults to 8080 for HTTP and 443 for HTTPS
+     * @param string $proxyUsername Leave blank if proxy has public access
+     * @param string $proxyPassword Leave blank if proxy has public access
+     * @param int $proxyAuthType defaults to CURLAUTH_BASIC (Basic authentication protocol); set to constant CURLAUTH_NTLM
+     *                           to use NTLM auth with proxy (has effect only when the client uses the HTTP 1.1 protocol)
+     * @return $this
+     */
+    public function setProxy($proxyHost, $proxyPort, $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1)
+    {
+        $this->proxy = $proxyHost;
+        $this->proxyport = $proxyPort;
+        $this->proxy_user = $proxyUsername;
+        $this->proxy_pass = $proxyPassword;
+        $this->proxy_authtype = $proxyAuthType;
+        return $this;
+    }
+
+    /**
+     * Enables/disables reception of compressed xml-rpc responses.
+     *
+     * This requires the "zlib" extension to be enabled in your php install. If it is, by default xmlrpc_client
+     * instances will enable reception of compressed content.
+     * Note that enabling reception of compressed responses merely adds some standard http headers to xml-rpc requests.
+     * It is up to the xml-rpc server to return compressed responses when receiving such requests.
+     *
+     * @param string $compMethod either 'gzip', 'deflate', 'any' or ''
+     * @return $this
+     */
+    public function setAcceptedCompression($compMethod)
+    {
+        if ($compMethod == 'any') {
+            $this->accepted_compression = array('gzip', 'deflate');
+        } elseif ($compMethod == false) {
+            $this->accepted_compression = array();
+        } else {
+            $this->accepted_compression = array($compMethod);
+        }
+        return $this;
+    }
+
+    /**
+     * Enables/disables http compression of xml-rpc request.
+     *
+     * This requires the "zlib" extension to be enabled in your php install.
+     * Take care when sending compressed requests: servers might not support them (and automatic fallback to
+     * uncompressed requests is not yet implemented).
+     *
+     * @param string $compMethod either 'gzip', 'deflate' or ''
+     * @return $this
+     * @deprecated use setOption
+     */
+    public function setRequestCompression($compMethod)
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        $this->request_compression = $compMethod;
+        return $this;
+    }
+
+    /**
+     * Adds a cookie to list of cookies that will be sent to server with every further request (useful e.g. for keeping
+     * session info outside the xml-rpc payload).
+     *
+     * NB: by default all cookies set via this method are sent to the server, regardless of path/domain/port. Taking
+     * advantage of those values is left to the single developer.
+     *
+     * @param string $name nb: will not be escaped in the request's http headers. Take care not to use CTL chars or
+     *                     separators!
+     * @param string $value
+     * @param string $path
+     * @param string $domain
+     * @param int $port do not use! Cookies are not separated by port
+     * @return $this
+     *
+     * @todo check correctness of urlencoding cookie value (copied from php way of doing it, but php is generally sending
+     *       response not requests. We do the opposite...)
+     * @todo strip invalid chars from cookie name? As per RFC 6265, we should follow RFC 2616, Section 2.2
+     * @todo drop/rename $port parameter. Cookies are not isolated by port!
+     * @todo feature-creep allow storing 'expires', 'secure', 'httponly' and 'samesite' cookie attributes (we could do
+     *       as php, and allow $path to be an array of attributes...)
+     */
+    public function setCookie($name, $value = '', $path = '', $domain = '', $port = null)
+    {
+        $this->cookies[$name]['value'] = rawurlencode($value);
+        if ($path || $domain || $port) {
+            $this->cookies[$name]['path'] = $path;
+            $this->cookies[$name]['domain'] = $domain;
+            $this->cookies[$name]['port'] = $port;
+        }
+        return $this;
+    }
+
+    /**
+     * Directly set cURL options, for extra flexibility (when in cURL mode).
+     *
+     * It allows e.g. to bind client to a specific IP interface / address.
+     *
+     * @param array $options
+     * @return $this
+     * @deprecated use setOption
+     */
+    public function setCurlOptions($options)
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        $this->extracurlopts = $options;
+        return $this;
+    }
+
+    /**
+     * @param int $useCurlMode self::USE_CURL_ALWAYS, self::USE_CURL_AUTO or self::USE_CURL_NEVER
+     * @return $this
+     * @deprecated use setOption
+     */
+    public function setUseCurl($useCurlMode)
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        $this->use_curl = $useCurlMode;
+        return $this;
+    }
+
+
+    /**
+     * Set user-agent string that will be used by this client instance in http headers sent to the server.
+     *
+     * The default user agent string includes the name of this library and the version number.
+     *
+     * @param string $agentString
+     * @return $this
+     * @deprecated use setOption
+     */
+    public function setUserAgent($agentString)
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        $this->user_agent = $agentString;
+        return $this;
+    }
+
+    /**
+     * @param null|int $component allowed values: PHP_URL_SCHEME, PHP_URL_HOST, PHP_URL_PORT, PHP_URL_PATH
+     * @return string|int Notes: the path component will include query string and fragment; NULL is a valid value for port
+     *                    (in which case the default port for http/https will be used);
+     * @throws ValueErrorException on unsupported component
+     */
+    public function getUrl($component = null)
+    {
+        if (is_int($component) || ctype_digit($component)) {
+            switch ($component) {
+                case PHP_URL_SCHEME:
+                    return $this->method;
+                case PHP_URL_HOST:
+                    return $this->server;
+                case PHP_URL_PORT:
+                    return $this->port;
+                case  PHP_URL_PATH:
+                    return $this->path;
+                case '':
+
+                default:
+                    throw new ValueErrorException("Unsupported component '$component'");
+            }
+        }
+
+        $url = $this->method . '://' . $this->server;
+        if ($this->port == 0 || ($this->port == 80 && in_array($this->method, array('http', 'http10', 'http11', 'h2c'))) ||
+            ($this->port == 443 && in_array($this->method, array('https', 'h2')))) {
+            return $url . $this->path;
+        } else {
+            return $url . ':' . $this->port . $this->path;
+        }
+    }
+
+    /**
+     * Send an xml-rpc request to the server.
+     *
+     * @param Request|Request[]|string $req The Request object, or an array of requests for using multicall, or the
+     *                                      complete xml representation of a request.
+     *                                      When sending an array of Request objects, the client will try to make use of
+     *                                      a single 'system.multicall' xml-rpc method call to forward to the server all
+     *                                      the requests in a single HTTP round trip, unless $this->no_multicall has
+     *                                      been previously set to TRUE (see the multicall method below), in which case
+     *                                      many consecutive xml-rpc requests will be sent. The method will return an
+     *                                      array of Response objects in both cases.
+     *                                      The third variant allows to build by hand (or any other means) a complete
+     *                                      xml-rpc request message, and send it to the server. $req should be a string
+     *                                      containing the complete xml representation of the request. It is e.g. useful
+     *                                      when, for maximal speed of execution, the request is serialized into a
+     *                                      string using the native php xml-rpc functions (see http://www.php.net/xmlrpc)
+     * @param integer $timeout deprecated. Connection timeout, in seconds, If unspecified, the timeout set with setOption
+     *                         will be used. If that is 0, a platform specific timeout will apply.
+     *                         This timeout value is passed to fsockopen(). It is also used for detecting server
+     *                         timeouts during communication (i.e. if the server does not send anything to the client
+     *                         for $timeout seconds, the connection will be closed).
+     * @param string $method deprecated. Use the same value in the constructor instead.
+     *                       Valid values are 'http', 'http11', 'https', 'h2' and 'h2c'. If left empty,
+     *                       the http protocol chosen during creation of the object will be used.
+     *                       Use 'h2' to make the lib attempt to use http/2 over a secure connection, and 'h2c'
+     *                       for http/2 without tls. Note that 'h2c' will not use the h2c 'upgrade' method, and be
+     *                       thus incompatible with any server/proxy not supporting http/2. This is because POST
+     *                       request are not compatible with h2c upgrade.
+     * @return Response|Response[] Note that the client will always return a Response object, even if the call fails
+     *
+     * @todo allow throwing exceptions instead of returning responses in case of failed calls and/or Fault responses
+     * @todo refactor: we now support many options besides connection timeout and http version to use. Why only privilege those?
+     */
+    public function send($req, $timeout = 0, $method = '')
+    {
+        if ($method !== '' || $timeout !== 0) {
+            $this->logDeprecation("Using non-default values for arguments 'method' and 'timeout' when calling method " . __METHOD__ . ' is deprecated');
+        }
+
+        // if user does not specify http protocol, use native method of this client
+        // (i.e. method set during call to constructor)
+        if ($method == '') {
+            $method = $this->method;
+        }
+
+        if ($timeout == 0) {
+            $timeout = $this->timeout;
+        }
+
+        if (is_array($req)) {
+            // $req is an array of Requests
+            /// @todo switch to the new syntax for multicall
+            return $this->multicall($req, $timeout, $method);
+        } elseif (is_string($req)) {
+            $n = new static::$requestClass('');
+            /// @todo we should somehow allow the caller to declare a custom contenttype too, esp. for the charset declaration
+            $n->setPayload($req);
+            $req = $n;
+        }
+
+        // where req is a Request
+        $req->setDebug($this->debug);
+
+        /// @todo we could be smarter about this and not force usage of curl for https if not present as well as use the
+        ///       presence of curl_extra_opts or socket_extra_opts as a hint
+        $useCurl = ($this->use_curl == self::USE_CURL_ALWAYS) || ($this->use_curl == self::USE_CURL_AUTO && (
+            in_array($method, array('https', 'http11', 'h2c', 'h2')) ||
+            ($this->username != '' && $this->authtype != 1) ||
+            ($this->proxy != '' && $this->proxy_user != '' && $this->proxy_authtype != 1)
+        ));
+
+        // BC - we go through sendPayloadCURL/sendPayloadSocket in case some subclass reimplemented those
+        if ($useCurl) {
+            $r = $this->sendPayloadCURL(
+                $req,
+                $this->server,
+                $this->port,
+                $timeout,
+                $this->username,
+                $this->password,
+                $this->authtype,
+                $this->cert,
+                $this->certpass,
+                $this->cacert,
+                $this->cacertdir,
+                $this->proxy,
+                $this->proxyport,
+                $this->proxy_user,
+                $this->proxy_pass,
+                $this->proxy_authtype,
+                // BC
+                $method == 'http11' ? 'http' : $method,
+                $this->keepalive,
+                $this->key,
+                $this->keypass,
+                $this->sslversion
+            );
+        } else {
+            $r = $this->sendPayloadSocket(
+                $req,
+                $this->server,
+                $this->port,
+                $timeout,
+                $this->username,
+                $this->password,
+                $this->authtype,
+                $this->cert,
+                $this->certpass,
+                $this->cacert,
+                $this->cacertdir,
+                $this->proxy,
+                $this->proxyport,
+                $this->proxy_user,
+                $this->proxy_pass,
+                $this->proxy_authtype,
+                $method,
+                $this->key,
+                $this->keypass,
+                $this->sslversion
+            );
+        }
+
+        return $r;
+    }
+
+    /**
+     * @param Request $req
+     * @param string $method
+     * @param string $server
+     * @param int $port
+     * @param string $path
+     * @param array $opts
+     * @return Response
+     */
+    protected function sendViaSocket($req, $method, $server, $port, $path, $opts)
+    {
+        /// @todo log a warning if passed an unsupported method
+
+        // Only create the payload if it was not created previously
+        /// @todo what if the request's payload was created with a different encoding?
+        ///       Also, if we do not call serialize(), the request will not set its content-type to have the charset declared
+        $payload = $req->getPayload();
+        if (empty($payload)) {
+            $payload = $req->serialize($opts['request_charset_encoding']);
+        }
+
+        // Deflate request body and set appropriate request headers
+        $encodingHdr = '';
+        if ($opts['request_compression'] == 'gzip' || $opts['request_compression'] == 'deflate') {
+            if ($opts['request_compression'] == 'gzip' && function_exists('gzencode')) {
+                $a = @gzencode($payload);
+                if ($a) {
+                    $payload = $a;
+                    $encodingHdr = "Content-Encoding: gzip\r\n";
+                } else {
+                    $this->getLogger()->warning('XML-RPC: ' . __METHOD__ . ': gzencode failure in compressing request');
+                }
+            } else if (function_exists('gzcompress')) {
+                $a = @gzcompress($payload);
+                if ($a) {
+                    $payload = $a;
+                    $encodingHdr = "Content-Encoding: deflate\r\n";
+                } else {
+                    $this->getLogger()->warning('XML-RPC: ' . __METHOD__ . ': gzcompress failure in compressing request');
+                }
+            } else {
+                $this->getLogger()->warning('XML-RPC: ' . __METHOD__ . ': desired request compression method is unsupported by this PHP install');
+            }
+        } else {
+            if ($opts['request_compression'] != '') {
+                $this->getLogger()->warning('XML-RPC: ' . __METHOD__ . ': desired request compression method is unsupported');
+            }
+        }
+
+        // thanks to Grant Rauscher
+        $credentials = '';
+        if ($opts['username'] != '') {
+            if ($opts['authtype'] != 1) {
+                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth is supported with HTTP 1.0');
+                return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['unsupported_option'],
+                    PhpXmlRpc::$xmlrpcerr['unsupported_option'] . ': only Basic auth is supported with HTTP 1.0');
+            }
+            $credentials = 'Authorization: Basic ' . base64_encode($opts['username'] . ':' . $opts['password']) . "\r\n";
+        }
+
+        $acceptedEncoding = '';
+        if (is_array($opts['accepted_compression']) && count($opts['accepted_compression'])) {
+            $acceptedEncoding = 'Accept-Encoding: ' . implode(', ', $opts['accepted_compression']) . "\r\n";
+        }
+
+        if ($port == 0) {
+            $port = ($method === 'https') ? 443 : 80;
+        }
+
+        $proxyCredentials = '';
+        if ($opts['proxy']) {
+            if ($opts['proxyport'] == 0) {
+                $opts['proxyport'] = 8080;
+            }
+            $connectServer = $opts['proxy'];
+            $connectPort = $opts['proxyport'];
+            $transport = 'tcp';
+            /// @todo check: should we not use https in some cases?
+            $uri = 'http://' . $server . ':' . $port . $path;
+            if ($opts['proxy_user'] != '') {
+                if ($opts['proxy_authtype'] != 1) {
+                    $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth to proxy is supported with HTTP 1.0');
+                    return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['unsupported_option'],
+                        PhpXmlRpc::$xmlrpcerr['unsupported_option'] . ': only Basic auth to proxy is supported with HTTP 1.0');
+                }
+                $proxyCredentials = 'Proxy-Authorization: Basic ' . base64_encode($opts['proxy_user'] . ':' .
+                    $opts['proxy_pass']) . "\r\n";
+            }
+        } else {
+            $connectServer = $server;
+            $connectPort = $port;
+            $transport = ($method === 'https') ? 'tls' : 'tcp';
+            $uri = $path;
+        }
+
+        // Cookie generation, as per RFC 6265
+        // NB: the following code does not honour 'expires', 'path' and 'domain' cookie attributes set to client obj by the user...
+        $cookieHeader = '';
+        if (count($opts['cookies'])) {
+            $version = '';
+            foreach ($opts['cookies'] as $name => $cookie) {
+                /// @todo should we sanitize the cookie value on behalf of the user? See setCookie comments
+                $cookieHeader .= ' ' . $name . '=' . $cookie['value'] . ";";
+            }
+            $cookieHeader = 'Cookie:' . $version . substr($cookieHeader, 0, -1) . "\r\n";
+        }
+
+        // omit port if default
+        if (($port == 80 && in_array($method, array('http', 'http10'))) || ($port == 443 && $method == 'https')) {
+            $port = '';
+        } else {
+            $port = ':' . $port;
+        }
+
+        $op = 'POST ' . $uri . " HTTP/1.0\r\n" .
+            'User-Agent: ' . $opts['user_agent'] . "\r\n" .
+            'Host: ' . $server . $port . "\r\n" .
+            $credentials .
+            $proxyCredentials .
+            $acceptedEncoding .
+            $encodingHdr .
+            'Accept-Charset: ' . implode(',', $opts['accepted_charset_encodings']) . "\r\n" .
+            $cookieHeader .
+            'Content-Type: ' . $req->getContentType() . "\r\nContent-Length: " .
+            strlen($payload) . "\r\n\r\n" .
+            $payload;
+
+        if ($opts['debug'] > 1) {
+            $this->getLogger()->debug("---SENDING---\n$op\n---END---");
+        }
+
+        $contextOptions = array();
+        if ($method == 'https') {
+            if ($opts['cert'] != '') {
+                $contextOptions['ssl']['local_cert'] = $opts['cert'];
+                if ($opts['certpass'] != '') {
+                    $contextOptions['ssl']['passphrase'] = $opts['certpass'];
+                }
+            }
+            if ($opts['cacert'] != '') {
+                $contextOptions['ssl']['cafile'] = $opts['cacert'];
+            }
+            if ($opts['cacertdir'] != '') {
+                $contextOptions['ssl']['capath'] = $opts['cacertdir'];
+            }
+            if ($opts['key'] != '') {
+                $contextOptions['ssl']['local_pk'] = $opts['key'];
+            }
+            $contextOptions['ssl']['verify_peer'] = $opts['verifypeer'];
+            $contextOptions['ssl']['verify_peer_name'] = $opts['verifypeer'];
+
+            if ($opts['sslversion'] != 0) {
+                /// @see https://www.php.net/manual/en/function.curl-setopt.php, https://www.php.net/manual/en/migration56.openssl.php
+                switch($opts['sslversion']) {
+                    /// @todo what does this map to? 1.0-1.3?
+                    //case 1: // TLSv1
+                    //    break;
+                    case 2: // SSLv2
+                        $contextOptions['ssl']['crypto_method'] = STREAM_CRYPTO_METHOD_SSLv2_CLIENT;
+                        break;
+                    case 3: // SSLv3
+                        $contextOptions['ssl']['crypto_method'] = STREAM_CRYPTO_METHOD_SSLv3_CLIENT;
+                        break;
+                    case 4: // TLSv1.0
+                        $contextOptions['ssl']['crypto_method'] = STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT;
+                        break;
+                    case 5: // TLSv1.1
+                        $contextOptions['ssl']['crypto_method'] = STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
+                        break;
+                    case 6: // TLSv1.2
+                        $contextOptions['ssl']['crypto_method'] = STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
+                        break;
+                    case 7: // TLSv1.3
+                        if (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT')) {
+                            $contextOptions['ssl']['crypto_method'] = STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT;
+                        } else {
+                            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['unsupported_option'],
+                                PhpXmlRpc::$xmlrpcerr['unsupported_option'] . ': TLS-1.3 only is supported with PHP 7.4 or later');
+                        }
+                        break;
+                    default:
+                        return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['unsupported_option'],
+                            PhpXmlRpc::$xmlrpcerr['unsupported_option'] . ': Unsupported required TLS version');
+                }
+            }
+        }
+
+        foreach ($opts['extracurlopts'] as $proto => $protoOpts) {
+            foreach ($protoOpts as $key => $val) {
+                $contextOptions[$proto][$key] = $val;
+            }
+        }
+
+        $context = stream_context_create($contextOptions);
+
+        if ($opts['timeout'] <= 0) {
+            $connectTimeout = ini_get('default_socket_timeout');
+        } else {
+            $connectTimeout = $opts['timeout'];
+        }
+
+        $this->errno = 0;
+        $this->errstr = '';
+
+        $fp = @stream_socket_client("$transport://$connectServer:$connectPort", $this->errno, $this->errstr, $connectTimeout,
+            STREAM_CLIENT_CONNECT, $context);
+        if ($fp) {
+            if ($opts['timeout'] > 0) {
+                stream_set_timeout($fp, $opts['timeout'], 0);
+            }
+        } else {
+            if ($this->errstr == '') {
+                $err = error_get_last();
+                $this->errstr = $err['message'];
+            }
+
+            $this->errstr = 'Connect error: ' . $this->errstr;
+            $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['http_error'], $this->errstr . ' (' . $this->errno . ')');
+
+            return $r;
+        }
+
+        if (!fputs($fp, $op, strlen($op))) {
+            fclose($fp);
+            $this->errstr = 'Write error';
+            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['http_error'], $this->errstr);
+        }
+
+        // Close socket before parsing.
+        // It should yield slightly better execution times, and make easier recursive calls (e.g. to follow http redirects)
+        $ipd = '';
+        do {
+            // shall we check for $data === FALSE?
+            // as per the manual, it signals an error
+            $ipd .= fread($fp, 32768);
+        } while (!feof($fp));
+        fclose($fp);
+
+        return $req->parseResponse($ipd, false, $opts['return_type']);
+    }
+
+    /**
+     * Contributed by Justin Miller
+     * Requires curl to be built into PHP
+     * NB: CURL versions before 7.11.10 cannot use proxy to talk to https servers!
+     *
+     * @param Request $req
+     * @param string $method
+     * @param string $server
+     * @param int $port
+     * @param string $path
+     * @param array $opts the keys/values match self::getOptions
+     * @return Response
+     *
+     * @todo the $path arg atm is ignored. What to do if it is != $this->path?
+     */
+    protected function sendViaCURL($req, $method, $server, $port, $path, $opts)
+    {
+        if (!function_exists('curl_init')) {
+            $this->errstr = 'CURL unavailable on this install';
+            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['no_curl'], PhpXmlRpc::$xmlrpcstr['no_curl']);
+        }
+        if ($method == 'https' || $method == 'h2') {
+            // q: what about installs where we get back a string, but curl is linked to other ssl libs than openssl?
+            if (($info = curl_version()) &&
+                ((is_string($info) && strpos($info, 'OpenSSL') === null) || (is_array($info) && !isset($info['ssl_version'])))
+            ) {
+                $this->errstr = 'SSL unavailable on this install';
+                return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['no_ssl'], PhpXmlRpc::$xmlrpcstr['no_ssl']);
+            }
+        }
+        if (($method == 'h2' && !defined('CURL_HTTP_VERSION_2_0')) ||
+            ($method == 'h2c' && !defined('CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE'))) {
+            $this->errstr = 'HTTP/2 unavailable on this install';
+            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['no_http2'], PhpXmlRpc::$xmlrpcstr['no_http2']);
+        }
+
+        // BC - we go through prepareCurlHandle in case some subclass reimplemented it
+        $curl = $this->prepareCurlHandle($req, $server, $port, $opts['timeout'], $opts['username'], $opts['password'],
+            $opts['authtype'], $opts['cert'], $opts['certpass'], $opts['cacert'], $opts['cacertdir'], $opts['proxy'],
+            $opts['proxyport'], $opts['proxy_user'], $opts['proxy_pass'], $opts['proxy_authtype'], $method,
+            $opts['keepalive'], $opts['key'], $opts['keypass'], $opts['sslversion']);
+
+        if (!$curl) {
+            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['curl_fail'], PhpXmlRpc::$xmlrpcstr['curl_fail'] .
+                ': error during curl initialization. Check php error log for details');
+        }
+
+        $result = curl_exec($curl);
+
+        if ($opts['debug'] > 1) {
+            $message = "---CURL INFO---\n";
+            foreach (curl_getinfo($curl) as $name => $val) {
+                if (is_array($val)) {
+                    $val = implode("\n", $val);
+                }
+                $message .= $name . ': ' . $val . "\n";
+            }
+            $message .= '---END---';
+            $this->getLogger()->debug($message);
+        }
+
+        if (!$result) {
+            /// @todo we should use a better check here - what if we get back '' or '0'?
+
+            $this->errstr = 'no response';
+            $resp = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['curl_fail'], PhpXmlRpc::$xmlrpcstr['curl_fail'] .
+                ': ' . curl_error($curl));
+            curl_close($curl);
+            if ($opts['keepalive']) {
+                $this->xmlrpc_curl_handle = null;
+            }
+        } else {
+            if (!$opts['keepalive']) {
+                curl_close($curl);
+            }
+            $resp = $req->parseResponse($result, true, $opts['return_type']);
+            if ($opts['keepalive']) {
+                /// @todo if we got back a 302 or 308, we should not reuse the curl handle for later calls
+                if ($resp->faultCode() == PhpXmlRpc::$xmlrpcerr['http_error']) {
+                    curl_close($curl);
+                    $this->xmlrpc_curl_handle = null;
+                }
+            }
+        }
+
+        return $resp;
+    }
+
+    /**
+     * @param Request $req
+     * @param string $method
+     * @param string $server
+     * @param int $port
+     * @param string $path
+     * @param array $opts the keys/values match self::getOptions
+     * @return \CurlHandle|resource|false
+     *
+     * @todo allow this method to either throw or return a Response, so that we can pass back to caller more info on errors
+     */
+    protected function createCURLHandle($req, $method, $server, $port, $path, $opts)
+    {
+        if ($port == 0) {
+            if (in_array($method, array('http', 'http10', 'http11', 'h2c'))) {
+                $port = 80;
+            } else {
+                $port = 443;
+            }
+        }
+
+        // Only create the payload if it was not created previously
+        $payload = $req->getPayload();
+        if (empty($payload)) {
+            $payload = $req->serialize($opts['request_charset_encoding']);
+        }
+
+        // Deflate request body and set appropriate request headers
+        $encodingHdr = '';
+        if (($opts['request_compression'] == 'gzip' || $opts['request_compression'] == 'deflate')) {
+            if ($opts['request_compression'] == 'gzip' && function_exists('gzencode')) {
+                $a = @gzencode($payload);
+                if ($a) {
+                    $payload = $a;
+                    $encodingHdr = 'Content-Encoding: gzip';
+                } else {
+                    $this->getLogger()->warning('XML-RPC: ' . __METHOD__ . ': gzencode failure in compressing request');
+                }
+            } else if (function_exists('gzcompress')) {
+                $a = @gzcompress($payload);
+                if ($a) {
+                    $payload = $a;
+                    $encodingHdr = 'Content-Encoding: deflate';
+                } else {
+                    $this->getLogger()->warning('XML-RPC: ' . __METHOD__ . ': gzcompress failure in compressing request');
+                }
+            } else {
+                $this->getLogger()->warning('XML-RPC: ' . __METHOD__ . ': desired request compression method is unsupported by this PHP install');
+            }
+        } else {
+            if ($opts['request_compression'] != '') {
+                $this->getLogger()->warning('XML-RPC: ' . __METHOD__ . ': desired request compression method is unsupported');
+            }
+        }
+
+        if (!$opts['keepalive'] || !$this->xmlrpc_curl_handle) {
+            if ($method == 'http11' || $method == 'http10' || $method == 'h2c') {
+                $protocol = 'http';
+            } else {
+                if ($method == 'h2') {
+                    $protocol = 'https';
+                } else {
+                    // http, https
+                    $protocol = $method;
+                    if (strpos($protocol, ':') !== false) {
+                        $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ": warning - attempted hacking attempt?. The curl protocol requested for the call is: '$protocol'");
+                        return false;
+                    }
+                }
+            }
+            $curl = curl_init($protocol . '://' . $server . ':' . $port . $path);
+            if (!$curl) {
+                return false;
+            }
+            if ($opts['keepalive']) {
+                $this->xmlrpc_curl_handle = $curl;
+            }
+        } else {
+            $curl = $this->xmlrpc_curl_handle;
+        }
+
+        // results into variable
+        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+
+        if ($opts['debug'] > 1) {
+            curl_setopt($curl, CURLOPT_VERBOSE, true);
+            /// @todo redirect curlopt_stderr to some stream which can be piped to the logger
+        }
+        curl_setopt($curl, CURLOPT_USERAGENT, $opts['user_agent']);
+        // required for XMLRPC: post the data
+        curl_setopt($curl, CURLOPT_POST, 1);
+        // the data
+        curl_setopt($curl, CURLOPT_POSTFIELDS, $payload);
+
+        // return the header too
+        curl_setopt($curl, CURLOPT_HEADER, 1);
+
+        // NB: if we set an empty string, CURL will add http header indicating
+        // ALL methods it is supporting. This is possibly a better option than letting the user tell what curl can / cannot do...
+        if (is_array($opts['accepted_compression']) && count($opts['accepted_compression'])) {
+            //curl_setopt($curl, CURLOPT_ENCODING, implode(',', $opts['accepted_compression']));
+            // empty string means 'any supported by CURL' (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
+            if (count($opts['accepted_compression']) == 1) {
+                curl_setopt($curl, CURLOPT_ENCODING, $opts['accepted_compression'][0]);
+            } else {
+                curl_setopt($curl, CURLOPT_ENCODING, '');
+            }
+        }
+        // extra headers
+        $headers = array('Content-Type: ' . $req->getContentType(), 'Accept-Charset: ' . implode(',', $opts['accepted_charset_encodings']));
+        // if no keepalive is wanted, let the server know it in advance
+        if (!$opts['keepalive']) {
+            $headers[] = 'Connection: close';
+        }
+        // request compression header
+        if ($encodingHdr) {
+            $headers[] = $encodingHdr;
+        }
+
+        // Fix the HTTP/1.1 417 Expectation Failed Bug (curl by default adds a 'Expect: 100-continue' header when POST
+        // size exceeds 1025 bytes, apparently)
+        $headers[] = 'Expect:';
+
+        curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
+        // timeout is borked
+        if ($opts['timeout']) {
+            curl_setopt($curl, CURLOPT_TIMEOUT, $opts['timeout'] == 1 ? 1 : $opts['timeout'] - 1);
+        }
+
+        switch ($method) {
+            case 'http10':
+                curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
+                break;
+            case 'http11':
+                curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
+                break;
+            case 'h2c':
+                if (defined('CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE')) {
+                    curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE);
+                } else {
+                    $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': warning. HTTP2 is not supported by the current PHP/curl install');
+                    curl_close($curl);
+                    return false;
+                }
+                break;
+            case 'h2':
+                curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
+                break;
+        }
+
+        if ($opts['username'] && $opts['password']) {
+            curl_setopt($curl, CURLOPT_USERPWD, $opts['username'] . ':' . $opts['password']);
+            if (defined('CURLOPT_HTTPAUTH')) {
+                curl_setopt($curl, CURLOPT_HTTPAUTH, $opts['authtype']);
+            } elseif ($opts['authtype'] != 1) {
+                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth is supported by the current PHP/curl install');
+                curl_close($curl);
+                return false;
+            }
+        }
+
+        // note: h2c is http2 without the https. No need to have it in this IF
+        if ($method == 'https' || $method == 'h2') {
+            // set cert file
+            if ($opts['cert']) {
+                curl_setopt($curl, CURLOPT_SSLCERT, $opts['cert']);
+            }
+            // set cert password
+            if ($opts['certpass']) {
+                curl_setopt($curl, CURLOPT_SSLCERTPASSWD, $opts['certpass']);
+            }
+            // whether to verify remote host's cert
+            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $opts['verifypeer']);
+            // set ca certificates file/dir
+            if ($opts['cacert']) {
+                curl_setopt($curl, CURLOPT_CAINFO, $opts['cacert']);
+            }
+            if ($opts['cacertdir']) {
+                curl_setopt($curl, CURLOPT_CAPATH, $opts['cacertdir']);
+            }
+            // set key file (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
+            if ($opts['key']) {
+                curl_setopt($curl, CURLOPT_SSLKEY, $opts['key']);
+            }
+            // set key password (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
+            if ($opts['keypass']) {
+                curl_setopt($curl, CURLOPT_SSLKEYPASSWD, $opts['keypass']);
+            }
+            // whether to verify cert's common name (CN); 0 for no, 1 to verify that it exists, and 2 to verify that
+            // it matches the hostname used
+            curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, $opts['verifyhost']);
+            // allow usage of different SSL versions
+            curl_setopt($curl, CURLOPT_SSLVERSION, $opts['sslversion']);
+        }
+
+        // proxy info
+        if ($opts['proxy']) {
+            if ($opts['proxyport'] == 0) {
+                $opts['proxyport'] = 8080; // NB: even for HTTPS, local connection is on port 8080
+            }
+            curl_setopt($curl, CURLOPT_PROXY, $opts['proxy'] . ':' . $opts['proxyport']);
+            if ($opts['proxy_user']) {
+                curl_setopt($curl, CURLOPT_PROXYUSERPWD, $opts['proxy_user'] . ':' . $opts['proxy_pass']);
+                if (defined('CURLOPT_PROXYAUTH')) {
+                    curl_setopt($curl, CURLOPT_PROXYAUTH, $opts['proxy_authtype']);
+                } elseif ($opts['proxy_authtype'] != 1) {
+                    $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth to proxy is supported by the current PHP/curl install');
+                    curl_close($curl);
+                    return false;
+                }
+            }
+        }
+
+        // NB: should we build cookie http headers by hand rather than let CURL do it?
+        // NB: the following code does not honour 'expires', 'path' and 'domain' cookie attributes set to client obj by the user...
+        if (count($opts['cookies'])) {
+            $cookieHeader = '';
+            foreach ($opts['cookies'] as $name => $cookie) {
+                $cookieHeader .= $name . '=' . $cookie['value'] . '; ';
+            }
+            curl_setopt($curl, CURLOPT_COOKIE, substr($cookieHeader, 0, -2));
+        }
+
+        foreach ($opts['extracurlopts'] as $opt => $val) {
+            curl_setopt($curl, $opt, $val);
+        }
+
+        if ($opts['debug'] > 1) {
+            $this->getLogger()->debug("---SENDING---\n$payload\n---END---");
+        }
+
+        return $curl;
+    }
+
+    /**
+     * Send an array of requests and return an array of responses.
+     *
+     * Unless $this->no_multicall has been set to true, it will try first to use one single xml-rpc call to server method
+     * system.multicall, and revert to sending many successive calls in case of failure.
+     * This failure is also stored in $this->no_multicall for subsequent calls.
+     * Unfortunately, there is no server error code universally used to denote the fact that multicall is unsupported,
+     * so there is no way to reliably distinguish between that and a temporary failure.
+     * If you are sure that server supports multicall and do not want to fallback to using many single calls, set the
+     * 2np parameter to FALSE.
+     *
+     * NB: trying to shoehorn extra functionality into existing syntax has resulted
+     * in pretty much convoluted code...
+     *
+     * @param Request[] $reqs an array of Request objects
+     * @param bool $noFallback When true, upon receiving an error during multicall, multiple single calls will not be
+     *                         attempted.
+     *                         Deprecated alternative, was: int - "connection timeout (in seconds). See the details in the
+     *                         docs for the send() method". Please use setOption instead to set a timeout
+     * @param string $method deprecated. Was: "the http protocol variant to be used. See the details in the docs for the send() method."
+     *                       Please use the constructor to set an http protocol variant.
+     * @param boolean $fallback deprecated. Was: "w"hen true, upon receiving an error during multicall, multiple single
+     *                          calls will be attempted"
+     * @return Response[]
+     */
+    public function multicall($reqs, $timeout = 0, $method = '', $fallback = true)
+    {
+        // BC
+        if (is_bool($timeout) && $fallback === true) {
+            $fallback = !$timeout;
+            $timeout = 0;
+        }
+
+        if ($method == '') {
+            $method = $this->method;
+        }
+
+        if (!$this->no_multicall) {
+            $results = $this->_try_multicall($reqs, $timeout, $method);
+            /// @todo how to handle the case of $this->return_type = xml?
+            if (is_array($results)) {
+                // System.multicall succeeded
+                return $results;
+            } else {
+                // either system.multicall is unsupported by server, or the call failed for some other reason.
+                // Feature creep: is there a way to tell apart unsupported multicall from other faults?
+                if ($fallback) {
+                    // Don't try it next time...
+                    $this->no_multicall = true;
+                } else {
+                    $result = $results;
+                }
+            }
+        } else {
+            // override fallback, in case careless user tries to do two
+            // opposite things at the same time
+            $fallback = true;
+        }
+
+        $results = array();
+        if ($fallback) {
+            // system.multicall is (probably) unsupported by server: emulate multicall via multiple requests
+            /// @todo use curl multi_ functions to make this quicker (see the implementation in the parallel.php demo)
+            foreach ($reqs as $req) {
+                $results[] = $this->send($req, $timeout, $method);
+            }
+        } else {
+            // user does NOT want to fallback on many single calls: since we should always return an array of responses,
+            // we return an array with the same error repeated n times
+            foreach ($reqs as $req) {
+                $results[] = $result;
+            }
+        }
+
+        return $results;
+    }
+
+    /**
+     * Attempt to boxcar $reqs via system.multicall.
+     *
+     * @param Request[] $reqs
+     * @param int $timeout
+     * @param string $method
+     * @return Response[]|Response a single Response when the call returned a fault / does not conform to what we expect
+     *                             from a multicall response
+     */
+    private function _try_multicall($reqs, $timeout, $method)
+    {
+        // Construct multicall request
+        $calls = array();
+        foreach ($reqs as $req) {
+            $call['methodName'] = new Value($req->method(), 'string');
+            $numParams = $req->getNumParams();
+            $params = array();
+            for ($i = 0; $i < $numParams; $i++) {
+                $params[$i] = $req->getParam($i);
+            }
+            $call['params'] = new Value($params, 'array');
+            $calls[] = new Value($call, 'struct');
+        }
+        $multiCall = new static::$requestClass('system.multicall');
+        $multiCall->addParam(new Value($calls, 'array'));
+
+        // Attempt RPC call
+        $result = $this->send($multiCall, $timeout, $method);
+
+        if ($result->faultCode() != 0) {
+            // call to system.multicall failed
+            return $result;
+        }
+
+        // Unpack responses.
+        $rets = $result->value();
+        $response = array();
+
+        if ($this->return_type == 'xml') {
+            for ($i = 0; $i < count($reqs); $i++) {
+                $response[] = new static::$responseClass($rets, 0, '', 'xml', $result->httpResponse());
+            }
+
+        } elseif ($this->return_type == 'phpvals') {
+            if (!is_array($rets)) {
+                // bad return type from system.multicall
+                return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
+                    PhpXmlRpc::$xmlrpcstr['multicall_error'] . ': not an array', 'phpvals', $result->httpResponse());
+            }
+            $numRets = count($rets);
+            if ($numRets != count($reqs)) {
+                // wrong number of return values.
+                return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
+                    PhpXmlRpc::$xmlrpcstr['multicall_error'] . ': incorrect number of responses', 'phpvals',
+                    $result->httpResponse());
+            }
+
+            for ($i = 0; $i < $numRets; $i++) {
+                $val = $rets[$i];
+                if (!is_array($val)) {
+                    return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
+                        PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i is not an array or struct",
+                        'phpvals', $result->httpResponse());
+                }
+                switch (count($val)) {
+                    case 1:
+                        if (!isset($val[0])) {
+                            // Bad value
+                            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
+                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has no value",
+                                'phpvals', $result->httpResponse());
+                        }
+                        // Normal return value
+                        $response[$i] = new static::$responseClass($val[0], 0, '', 'phpvals', $result->httpResponse());
+                        break;
+                    case 2:
+                        /// @todo remove usage of @: it is apparently quite slow
+                        $code = @$val['faultCode'];
+                        if (!is_int($code)) {
+                            /// @todo should we check that it is != 0?
+                            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
+                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has invalid or no faultCode",
+                                'phpvals', $result->httpResponse());
+                        }
+                        $str = @$val['faultString'];
+                        if (!is_string($str)) {
+                            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
+                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has invalid or no FaultString",
+                                'phpvals', $result->httpResponse());
+                        }
+                        $response[$i] = new static::$responseClass(0, $code, $str, 'phpvals', $result->httpResponse());
+                        break;
+                    default:
+                        return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
+                            PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has too many items",
+                            'phpvals', $result->httpResponse());
+                }
+            }
+
+        } else {
+            // return type == 'xmlrpcvals'
+            if ($rets->kindOf() != 'array') {
+                return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
+                    PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i is not an array", 'xmlrpcvals',
+                    $result->httpResponse());
+            }
+            $numRets = $rets->count();
+            if ($numRets != count($reqs)) {
+                // wrong number of return values.
+                return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
+                    PhpXmlRpc::$xmlrpcstr['multicall_error'] . ': incorrect number of responses', 'xmlrpcvals',
+                    $result->httpResponse());
+            }
+
+            foreach ($rets as $i => $val) {
+                switch ($val->kindOf()) {
+                    case 'array':
+                        if ($val->count() != 1) {
+                            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
+                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has too many items",
+                                'phpvals', $result->httpResponse());
+                        }
+                        // Normal return value
+                        $response[] = new static::$responseClass($val[0], 0, '', 'xmlrpcvals', $result->httpResponse());
+                        break;
+                    case 'struct':
+                        if ($val->count() != 2) {
+                            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
+                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has too many items",
+                                'phpvals', $result->httpResponse());
+                        }
+                        /** @var Value $code */
+                        $code = $val['faultCode'];
+                        if ($code->kindOf() != 'scalar' || $code->scalarTyp() != 'int') {
+                            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
+                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has invalid or no faultCode",
+                                'xmlrpcvals', $result->httpResponse());
+                        }
+                        /** @var Value $str */
+                        $str = $val['faultString'];
+                        if ($str->kindOf() != 'scalar' || $str->scalarTyp() != 'string') {
+                            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
+                                PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i has invalid or no faultCode",
+                                'xmlrpcvals', $result->httpResponse());
+                        }
+                        $response[] = new static::$responseClass(0, $code->scalarVal(), $str->scalarVal(), 'xmlrpcvals', $result->httpResponse());
+                        break;
+                    default:
+                        return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['multicall_error'],
+                            PhpXmlRpc::$xmlrpcstr['multicall_error'] . ": response element $i is not an array or struct",
+                            'xmlrpcvals', $result->httpResponse());
+                }
+            }
+        }
+
+        return $response;
+    }
+
+    // *** BC layer ***
+
+    /**
+     * @deprecated
+     *
+     * @param Request $req
+     * @param string $server
+     * @param int $port
+     * @param int $timeout
+     * @param string $username
+     * @param string $password
+     * @param int $authType
+     * @param string $proxyHost
+     * @param int $proxyPort
+     * @param string $proxyUsername
+     * @param string $proxyPassword
+     * @param int $proxyAuthType
+     * @param string $method
+     * @return Response
+     */
+    protected function sendPayloadHTTP10($req, $server, $port, $timeout = 0, $username = '', $password = '',
+        $authType = 1, $proxyHost = '', $proxyPort = 0, $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1,
+        $method = 'http')
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        return $this->sendPayloadSocket($req, $server, $port, $timeout, $username, $password, $authType, null, null,
+            null, null, $proxyHost, $proxyPort, $proxyUsername, $proxyPassword, $proxyAuthType, $method);
+    }
+
+    /**
+     * @deprecated
+     *
+     * @param Request $req
+     * @param string $server
+     * @param int $port
+     * @param int $timeout
+     * @param string $username
+     * @param string $password
+     * @param int $authType
+     * @param string $cert
+     * @param string $certPass
+     * @param string $caCert
+     * @param string $caCertDir
+     * @param string $proxyHost
+     * @param int $proxyPort
+     * @param string $proxyUsername
+     * @param string $proxyPassword
+     * @param int $proxyAuthType
+     * @param bool $keepAlive
+     * @param string $key
+     * @param string $keyPass
+     * @param int $sslVersion
+     * @return Response
+     */
+    protected function sendPayloadHTTPS($req, $server, $port, $timeout = 0, $username = '', $password = '',
+        $authType = 1, $cert = '', $certPass = '', $caCert = '', $caCertDir = '', $proxyHost = '', $proxyPort = 0,
+        $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $keepAlive = false, $key = '', $keyPass = '',
+        $sslVersion = 0)
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        return $this->sendPayloadCURL($req, $server, $port, $timeout, $username,
+            $password, $authType, $cert, $certPass, $caCert, $caCertDir, $proxyHost, $proxyPort,
+            $proxyUsername, $proxyPassword, $proxyAuthType, 'https', $keepAlive, $key, $keyPass, $sslVersion);
+    }
+
+    /**
+     * @deprecated
+     *
+     * @param Request $req
+     * @param string $server
+     * @param int $port
+     * @param int $timeout
+     * @param string $username
+     * @param string $password
+     * @param int $authType only value supported is 1
+     * @param string $cert
+     * @param string $certPass
+     * @param string $caCert
+     * @param string $caCertDir
+     * @param string $proxyHost
+     * @param int $proxyPort
+     * @param string $proxyUsername
+     * @param string $proxyPassword
+     * @param int $proxyAuthType only value supported is 1
+     * @param string $method 'http' (synonym for 'http10'), 'http10' or 'https'
+     * @param string $key
+     * @param string $keyPass @todo not implemented yet.
+     * @param int $sslVersion @todo not implemented yet. See http://php.net/manual/en/migration56.openssl.php
+     * @return Response
+     */
+    protected function sendPayloadSocket($req, $server, $port, $timeout = 0, $username = '', $password = '',
+        $authType = 1, $cert = '', $certPass = '', $caCert = '', $caCertDir = '', $proxyHost = '', $proxyPort = 0,
+        $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $method = 'http', $key = '', $keyPass = '',
+        $sslVersion = 0)
+    {
+        $this->logDeprecationUnlessCalledBy('send');
+
+        return $this->sendViaSocket($req, $method, $server, $port, $this->path, array(
+            'accepted_charset_encodings' => $this->accepted_charset_encodings,
+            'accepted_compression' => $this->accepted_compression,
+            'authtype' => $authType,
+            'cacert' => $caCert,
+            'cacertdir' => $caCertDir,
+            'cert' => $cert,
+            'certpass' => $certPass,
+            'cookies' => $this->cookies,
+            'debug' => $this->debug,
+            'extracurlopts' => $this->extracurlopts,
+            'extrasockopts' => $this->extrasockopts,
+            'keepalive' => $this->keepalive,
+            'key' => $key,
+            'keypass' => $keyPass,
+            'no_multicall' => $this->no_multicall,
+            'password' => $password,
+            'proxy' => $proxyHost,
+            'proxy_authtype' => $proxyAuthType,
+            'proxy_pass' => $proxyPassword,
+            'proxyport' => $proxyPort,
+            'proxy_user' => $proxyUsername,
+            'request_charset_encoding' => $this->request_charset_encoding,
+            'request_compression' => $this->request_compression,
+            'return_type' => $this->return_type,
+            'sslversion' => $sslVersion,
+            'timeout' => $timeout,
+            'username' => $username,
+            'user_agent' => $this->user_agent,
+            'use_curl' => $this->use_curl,
+            'verifyhost' => $this->verifyhost,
+            'verifypeer' => $this->verifypeer,
+        ));
+    }
+
+    /**
+     * @deprecated
+     *
+     * @param Request $req
+     * @param string $server
+     * @param int $port
+     * @param int $timeout
+     * @param string $username
+     * @param string $password
+     * @param int $authType
+     * @param string $cert
+     * @param string $certPass
+     * @param string $caCert
+     * @param string $caCertDir
+     * @param string $proxyHost
+     * @param int $proxyPort
+     * @param string $proxyUsername
+     * @param string $proxyPassword
+     * @param int $proxyAuthType
+     * @param string $method 'http' (let curl decide), 'http10', 'http11', 'https', 'h2c' or 'h2'
+     * @param bool $keepAlive
+     * @param string $key
+     * @param string $keyPass
+     * @param int $sslVersion
+     * @return Response
+     */
+    protected function sendPayloadCURL($req, $server, $port, $timeout = 0, $username = '', $password = '',
+        $authType = 1, $cert = '', $certPass = '', $caCert = '', $caCertDir = '', $proxyHost = '', $proxyPort = 0,
+        $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $method = 'https', $keepAlive = false, $key = '',
+        $keyPass = '', $sslVersion = 0)
+    {
+        $this->logDeprecationUnlessCalledBy('send');
+
+        return $this->sendViaCURL($req, $method, $server, $port, $this->path, array(
+            'accepted_charset_encodings' => $this->accepted_charset_encodings,
+            'accepted_compression' => $this->accepted_compression,
+            'authtype' => $authType,
+            'cacert' => $caCert,
+            'cacertdir' => $caCertDir,
+            'cert' => $cert,
+            'certpass' => $certPass,
+            'cookies' => $this->cookies,
+            'debug' => $this->debug,
+            'extracurlopts' => $this->extracurlopts,
+            'extrasockopts' => $this->extrasockopts,
+            'keepalive' => $keepAlive,
+            'key' => $key,
+            'keypass' => $keyPass,
+            'no_multicall' => $this->no_multicall,
+            'password' => $password,
+            'proxy' => $proxyHost,
+            'proxy_authtype' => $proxyAuthType,
+            'proxy_pass' => $proxyPassword,
+            'proxyport' => $proxyPort,
+            'proxy_user' => $proxyUsername,
+            'request_charset_encoding' => $this->request_charset_encoding,
+            'request_compression' => $this->request_compression,
+            'return_type' => $this->return_type,
+            'sslversion' => $sslVersion,
+            'timeout' => $timeout,
+            'username' => $username,
+            'user_agent' => $this->user_agent,
+            'use_curl' => $this->use_curl,
+            'verifyhost' => $this->verifyhost,
+            'verifypeer' => $this->verifypeer,
+        ));
+    }
+
+    /**
+     * @deprecated
+     *
+     * @param $req
+     * @param $server
+     * @param $port
+     * @param $timeout
+     * @param $username
+     * @param $password
+     * @param $authType
+     * @param $cert
+     * @param $certPass
+     * @param $caCert
+     * @param $caCertDir
+     * @param $proxyHost
+     * @param $proxyPort
+     * @param $proxyUsername
+     * @param $proxyPassword
+     * @param $proxyAuthType
+     * @param $method
+     * @param $keepAlive
+     * @param $key
+     * @param $keyPass
+     * @param $sslVersion
+     * @return false|\CurlHandle|resource
+     */
+    protected function prepareCurlHandle($req, $server, $port, $timeout = 0, $username = '', $password = '',
+         $authType = 1, $cert = '', $certPass = '', $caCert = '', $caCertDir = '', $proxyHost = '', $proxyPort = 0,
+         $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $method = 'https', $keepAlive = false, $key = '',
+         $keyPass = '', $sslVersion = 0)
+    {
+        $this->logDeprecationUnlessCalledBy('sendViaCURL');
+
+        return $this->createCURLHandle($req, $method, $server, $port, $this->path, array(
+            'accepted_charset_encodings' => $this->accepted_charset_encodings,
+            'accepted_compression' => $this->accepted_compression,
+            'authtype' => $authType,
+            'cacert' => $caCert,
+            'cacertdir' => $caCertDir,
+            'cert' => $cert,
+            'certpass' => $certPass,
+            'cookies' => $this->cookies,
+            'debug' => $this->debug,
+            'extracurlopts' => $this->extracurlopts,
+            'keepalive' => $keepAlive,
+            'key' => $key,
+            'keypass' => $keyPass,
+            'no_multicall' => $this->no_multicall,
+            'password' => $password,
+            'proxy' => $proxyHost,
+            'proxy_authtype' => $proxyAuthType,
+            'proxy_pass' => $proxyPassword,
+            'proxyport' => $proxyPort,
+            'proxy_user' => $proxyUsername,
+            'request_charset_encoding' => $this->request_charset_encoding,
+            'request_compression' => $this->request_compression,
+            'return_type' => $this->return_type,
+            'sslversion' => $sslVersion,
+            'timeout' => $timeout,
+            'username' => $username,
+            'user_agent' => $this->user_agent,
+            'use_curl' => $this->use_curl,
+            'verifyhost' => $this->verifyhost,
+            'verifypeer' => $this->verifypeer,
+        ));
+    }
+
+    // we have to make this return by ref in order to allow calls such as `$resp->_cookies['name'] = ['value' => 'something'];`
+    public function &__get($name)
+    {
+        if (in_array($name, static::$options)) {
+            $this->logDeprecation('Getting property Client::' . $name . ' is deprecated');
+            return $this->$name;
+        }
+
+        switch ($name) {
+            case 'errno':
+            case 'errstr':
+            case 'method':
+            case 'server':
+            case 'port':
+            case 'path':
+                $this->logDeprecation('Getting property Client::' . $name . ' is deprecated');
+                return $this->$name;
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+                $result = null;
+                return $result;
+        }
+    }
+
+    public function __set($name, $value)
+    {
+        if (in_array($name, static::$options)) {
+            $this->logDeprecation('Setting property Client::' . $name . ' is deprecated');
+            $this->$name = $value;
+            return;
+        }
+
+        switch ($name) {
+            case 'errno':
+            case 'errstr':
+            case 'method':
+            case 'server':
+            case 'port':
+            case 'path':
+                $this->logDeprecation('Setting property Client::' . $name . ' is deprecated');
+                $this->$name = $value;
+                return;
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __set(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+        }
+    }
+
+    public function __isset($name)
+    {
+        if (in_array($name, static::$options)) {
+            $this->logDeprecation('Checking property Client::' . $name . ' is deprecated');
+            return isset($this->$name);
+        }
+
+        switch ($name) {
+            case 'errno':
+            case 'errstr':
+            case 'method':
+            case 'server':
+            case 'port':
+            case 'path':
+                $this->logDeprecation('Checking property Client::' . $name . ' is deprecated');
+                return isset($this->$name);
+            default:
+                return false;
+        }
+    }
+
+    public function __unset($name)
+    {
+        if (in_array($name, static::$options)) {
+            $this->logDeprecation('Unsetting property Client::' . $name . ' is deprecated');
+            unset($this->$name);
+            return;
+        }
+
+        switch ($name) {
+            case 'errno':
+            case 'errstr':
+            case 'method':
+            case 'server':
+            case 'port':
+            case 'path':
+                $this->logDeprecation('Unsetting property Client::' . $name . ' is deprecated');
+                unset($this->$name);
+                return;
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __unset(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+        }
+    }
+}

+ 388 - 0
includes/phpxmlrpc/Encoder.php

@@ -0,0 +1,388 @@
+<?php
+
+namespace PhpXmlRpc;
+
+use PhpXmlRpc\Helper\XMLParser;
+use PhpXmlRpc\Traits\LoggerAware;
+use PhpXmlRpc\Traits\ParserAware;
+
+/**
+ * A helper class to easily convert between Value objects and php native values.
+ *
+ * @todo implement an interface
+ * @todo add class constants for the options values
+ */
+class Encoder
+{
+    use LoggerAware;
+    use ParserAware;
+
+    /**
+     * Takes an xml-rpc Value in object instance and translates it into native PHP types, recursively.
+     * Works with xml-rpc Request objects as input, too.
+     * Xmlrpc dateTime values will be converted to strings or DateTime objects depending on an $options parameter
+     * Supports i8 and NIL xml-rpc values without the need for specific options.
+     * Both xml-rpc arrays and structs are decoded into PHP arrays, with the exception described below:
+     * Given proper options parameter, can rebuild generic php object instances (provided those have been encoded to
+     * xml-rpc format using a corresponding option in php_xmlrpc_encode()).
+     * PLEASE NOTE that rebuilding php objects involves calling their constructor function.
+     * This means that the remote communication end can decide which php code will get executed on your server, leaving
+     * the door possibly open to 'php-injection' style of attacks (provided you have some classes defined on your server
+     * that might wreak havoc if instances are built outside an appropriate context).
+     * Make sure you trust the remote server/client before enabling this!
+     *
+     * @author Dan Libby
+     *
+     * @param Value|Request $xmlrpcVal
+     * @param array $options accepted elements:
+     *                      - 'decode_php_objs': if set in the options array, xml-rpc structs can be decoded into php
+     *                         objects, see the details above;
+     *                      - 'dates_as_objects': when set xml-rpc dateTimes are decoded as php DateTime objects
+     *                      - 'extension_api': reserved for usage by phpxmlrpc-polyfill
+     * @return mixed
+     *
+     * Feature creep -- add an option to allow converting xml-rpc dateTime values to unix timestamps (integers)
+     */
+    public function decode($xmlrpcVal, $options = array())
+    {
+        switch ($xmlrpcVal->kindOf()) {
+            case 'scalar':
+                if (in_array('extension_api', $options)) {
+                    $val = $xmlrpcVal->scalarVal();
+                    $typ = $xmlrpcVal->scalarTyp();
+                    switch ($typ) {
+                        case 'dateTime.iso8601':
+                            $xmlrpcVal = array(
+                                'xmlrpc_type' => 'datetime',
+                                'scalar' => $val,
+                                'timestamp' => \PhpXmlRpc\Helper\Date::iso8601Decode($val)
+                            );
+                            return (object)$xmlrpcVal;
+                        case 'base64':
+                            $xmlrpcVal = array(
+                                'xmlrpc_type' => 'base64',
+                                'scalar' => $val
+                            );
+                            return (object)$xmlrpcVal;
+                        case 'string':
+                            if (isset($options['extension_api_encoding'])) {
+                                // if iconv is not available, we use mb_convert_encoding
+                                if (function_exists('iconv')) {
+                                    $dval = @iconv('UTF-8', $options['extension_api_encoding'], $val);
+                                } elseif (function_exists('mb_convert_encoding')) {
+                                    /// @todo check for discrepancies between the supported charset names
+                                    $dval = @mb_convert_encoding($val, $options['extension_api_encoding'], 'UTF-8');
+                                } else {
+                                    $dval = false;
+                                }
+                                if ($dval !== false) {
+                                    return $dval;
+                                }
+                            }
+                            // break through voluntarily
+                        default:
+                            return $val;
+                    }
+                }
+                if (in_array('dates_as_objects', $options) && $xmlrpcVal->scalarTyp() == 'dateTime.iso8601') {
+                    // we return a Datetime object instead of a string; since now the constructor of xml-rpc value accepts
+                    // safely string, int and DateTimeInterface, we cater to all 3 cases here
+                    $out = $xmlrpcVal->scalarVal();
+                    if (is_string($out)) {
+                        $out = strtotime($out);
+                        // NB: if the string does not convert into a timestamp, this will return false.
+                        // We avoid logging an error here, as we presume it was already done when parsing the xml
+                        /// @todo we could return null, to be more in line with what the XMLParser does...
+                    }
+                    if (is_int($out)) {
+                        $result = new \DateTime();
+                        $result->setTimestamp($out);
+
+                        return $result;
+                    } elseif (is_a($out, 'DateTimeInterface') || is_a($out, 'DateTime')) {
+                        return $out;
+                    }
+                }
+                return $xmlrpcVal->scalarVal();
+
+            case 'array':
+                $arr = array();
+                foreach ($xmlrpcVal as $value) {
+                    $arr[] = $this->decode($value, $options);
+                }
+                return $arr;
+
+            case 'struct':
+                // If user said so, try to rebuild php objects for specific struct vals.
+                /// @todo should we raise a warning for class not found?
+                // shall we check for proper subclass of xml-rpc value instead of presence of _php_class to detect
+                // what we can do?
+                if (in_array('decode_php_objs', $options) && $xmlrpcVal->_php_class != ''
+                    && class_exists($xmlrpcVal->_php_class)
+                ) {
+                    $obj = @new $xmlrpcVal->_php_class();
+                    foreach ($xmlrpcVal as $key => $value) {
+                        $obj->$key = $this->decode($value, $options);
+                    }
+                    return $obj;
+                } else {
+                    $arr = array();
+                    foreach ($xmlrpcVal as $key => $value) {
+                        $arr[$key] = $this->decode($value, $options);
+                    }
+                    return $arr;
+                }
+
+            case 'msg':
+                $paramCount = $xmlrpcVal->getNumParams();
+                $arr = array();
+                for ($i = 0; $i < $paramCount; $i++) {
+                    $arr[] = $this->decode($xmlrpcVal->getParam($i), $options);
+                }
+                return $arr;
+
+            /// @todo throw on unsupported type
+        }
+    }
+
+    /**
+     * Takes native php types and encodes them into xml-rpc Value objects, recursively.
+     * PHP strings, integers, floats and booleans have a straightforward encoding - note that integers will _not_ be
+     * converted to xml-rpc <i8> elements, even if they exceed the 32-bit range.
+     * PHP arrays will be encoded to either xml-rpc structs or arrays, depending on whether they are hashes
+     * or plain 0..N integer indexed.
+     * PHP objects will be encoded into xml-rpc structs, except if they implement DateTimeInterface, in which case they
+     * will be encoded as dateTime values.
+     * PhpXmlRpc\Value objects will not be double-encoded - which makes it possible to pass in a pre-created base64 Value
+     * as part of a php array.
+     * If given a proper $options parameter, php object instances will be encoded into 'special' xml-rpc values, that can
+     * later be decoded into php object instances by calling php_xmlrpc_decode() with a corresponding option.
+     * PHP resource and NULL variables will be converted into uninitialized Value objects (which will lead to invalid
+     * xml-rpc when later serialized); to support encoding of the latter use the appropriate $options parameter.
+     *
+     * @author Dan Libby
+     *
+     * @param mixed $phpVal the value to be converted into an xml-rpc value object
+     * @param array $options can include:
+     *                       - 'encode_php_objs' when set, some out-of-band info will be added to the xml produced by
+     *                         serializing the built Value, which can later be decoced by this library to rebuild an
+     *                         instance of the same php object
+     *                       - 'auto_dates': when set, any string which respects the xml-rpc datetime format will be converted to a dateTime Value
+     *                       - 'null_extension': when set, php NULL values will be converted to an xml-rpc <NIL> (or <EX:NIL>) Value
+     *                       - 'extension_api': reserved for usage by phpxmlrpc-polyfill
+     * @return Value
+     *
+     * Feature creep -- could support more types via optional type argument (string => datetime support has been added,
+     * ??? => base64 not yet). Also: allow auto-encoding of integers to i8 when too-big to fit into i4
+     */
+    public function encode($phpVal, $options = array())
+    {
+        $type = gettype($phpVal);
+        switch ($type) {
+            case 'string':
+                if (in_array('auto_dates', $options) && preg_match(PhpXmlRpc::$xmlrpc_datetime_format, $phpVal)) {
+                    $xmlrpcVal = new Value($phpVal, Value::$xmlrpcDateTime);
+                } else {
+                    $xmlrpcVal = new Value($phpVal, Value::$xmlrpcString);
+                }
+                break;
+            case 'integer':
+                $xmlrpcVal = new Value($phpVal, Value::$xmlrpcInt);
+                break;
+            case 'double':
+                $xmlrpcVal = new Value($phpVal, Value::$xmlrpcDouble);
+                break;
+            case 'boolean':
+                $xmlrpcVal = new Value($phpVal, Value::$xmlrpcBoolean);
+                break;
+            case 'array':
+                // A shorter one-liner would be
+                //     $tmp = array_diff(array_keys($phpVal), range(0, count($phpVal)-1));
+                // but execution time skyrockets!
+                $j = 0;
+                $arr = array();
+                $ko = false;
+                foreach ($phpVal as $key => $val) {
+                    $arr[$key] = $this->encode($val, $options);
+                    if (!$ko && $key !== $j) {
+                        $ko = true;
+                    }
+                    $j++;
+                }
+                if ($ko) {
+                    $xmlrpcVal = new Value($arr, Value::$xmlrpcStruct);
+                } else {
+                    $xmlrpcVal = new Value($arr, Value::$xmlrpcArray);
+                }
+                break;
+            case 'object':
+                if (is_a($phpVal, 'PhpXmlRpc\Value')) {
+                    $xmlrpcVal = $phpVal;
+                // DateTimeInterface is not present in php 5.4...
+                } elseif (is_a($phpVal, 'DateTimeInterface') || is_a($phpVal, 'DateTime')) {
+                    $xmlrpcVal = new Value($phpVal->format('Ymd\TH:i:s'), Value::$xmlrpcDateTime);
+                } elseif (in_array('extension_api', $options) && $phpVal instanceof \stdClass && isset($phpVal->xmlrpc_type)) {
+                    // Handle the 'pre-converted' base64 and datetime values
+                    if (isset($phpVal->scalar)) {
+                        switch ($phpVal->xmlrpc_type) {
+                            case 'base64':
+                                $xmlrpcVal = new Value($phpVal->scalar, Value::$xmlrpcBase64);
+                                break;
+                            case 'datetime':
+                                $xmlrpcVal = new Value($phpVal->scalar, Value::$xmlrpcDateTime);
+                                break;
+                            default:
+                                $xmlrpcVal = new Value();
+                        }
+                    } else {
+                        $xmlrpcVal = new Value();
+                    }
+
+                } else {
+                    $arr = array();
+                    foreach ($phpVal as $k => $v) {
+                        $arr[$k] = $this->encode($v, $options);
+                    }
+                    $xmlrpcVal = new Value($arr, Value::$xmlrpcStruct);
+                    if (in_array('encode_php_objs', $options)) {
+                        // let's save original class name into xml-rpc value: it might be useful later on...
+                        $xmlrpcVal->_php_class = get_class($phpVal);
+                    }
+                }
+                break;
+            case 'NULL':
+                if (in_array('extension_api', $options)) {
+                    $xmlrpcVal = new Value('', Value::$xmlrpcString);
+                } elseif (in_array('null_extension', $options)) {
+                    $xmlrpcVal = new Value('', Value::$xmlrpcNull);
+                } else {
+                    $xmlrpcVal = new Value();
+                }
+                break;
+            case 'resource':
+                if (in_array('extension_api', $options)) {
+                    $xmlrpcVal = new Value((int)$phpVal, Value::$xmlrpcInt);
+                } else {
+                    $xmlrpcVal = new Value();
+                }
+                break;
+            // catch "user function", "unknown type"
+            default:
+                // it has to return an empty object in case, not a boolean. (giancarlo pinerolo)
+                $xmlrpcVal = new Value();
+                break;
+        }
+
+        return $xmlrpcVal;
+    }
+
+    /**
+     * Convert the xml representation of a method response, method request or single
+     * xml-rpc value into the appropriate object (a.k.a. deserialize).
+     *
+     * @param string $xmlVal
+     * @param array $options unused atm
+     * @return Value|Request|Response|false false on error, or an instance of either Value, Request or Response
+     *
+     * @todo is this a good name/class for this method? It does something quite different from 'decode' after all
+     *       (returning objects vs returns plain php values)... In fact, it belongs rather to a Parser class
+     * @todo feature creep -- we should allow an option to return php native types instead of PhpXmlRpc objects instances
+     * @todo feature creep -- allow source charset to be passed in as an option, in case the xml misses its declaration
+     * @todo feature creep -- allow expected type (val/req/resp) to be passed in as an option
+     */
+    public function decodeXml($xmlVal, $options = array())
+    {
+        // 'guestimate' encoding
+        $valEncoding = XMLParser::guessEncoding('', $xmlVal);
+        if ($valEncoding != '') {
+
+            // Since parsing will fail if
+            // - charset is not specified in the xml declaration,
+            // - the encoding is not UTF8 and
+            // - there are non-ascii chars in the text,
+            // we try to work round that...
+            // The following code might be better for mb_string enabled installs, but makes the lib about 200% slower...
+            //if (!is_valid_charset($valEncoding, array('UTF-8'))
+            if (!in_array($valEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($xmlVal)) {
+                if (function_exists('mb_convert_encoding')) {
+                    $xmlVal = mb_convert_encoding($xmlVal, 'UTF-8', $valEncoding);
+                } else {
+                    if ($valEncoding == 'ISO-8859-1') {
+                        $xmlVal = utf8_encode($xmlVal);
+                    } else {
+                        $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': invalid charset encoding of xml text: ' . $valEncoding);
+                    }
+                }
+            }
+        }
+
+        // What if internal encoding is not in one of the 3 allowed? We use the broadest one, i.e. utf8!
+        /// @todo with php < 5.6, this does not work. We should add a manual conversion of the xml string to UTF8
+        if (in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
+            $parserOptions = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
+        } else {
+            $parserOptions = array(XML_OPTION_TARGET_ENCODING => 'UTF-8', 'target_charset' => PhpXmlRpc::$xmlrpc_internalencoding);
+        }
+
+        $xmlRpcParser = $this->getParser();
+        $_xh = $xmlRpcParser->parse(
+            $xmlVal,
+            XMLParser::RETURN_XMLRPCVALS,
+            XMLParser::ACCEPT_REQUEST | XMLParser::ACCEPT_RESPONSE | XMLParser::ACCEPT_VALUE | XMLParser::ACCEPT_FAULT,
+            $parserOptions
+        );
+        // BC
+        if (!is_array($_xh)) {
+            $_xh = $xmlRpcParser->_xh;
+        }
+
+        if ($_xh['isf'] > 1) {
+            // test that $_xh['value'] is an obj, too???
+
+            $this->getLogger()->error('XML-RPC: ' . $_xh['isf_reason']);
+
+            return false;
+        }
+
+        switch ($_xh['rt']) {
+            case 'methodresponse':
+                $v = $_xh['value'];
+                if ($_xh['isf'] == 1) {
+                    /** @var Value $vc */
+                    $vc = $v['faultCode'];
+                    /** @var Value $vs */
+                    $vs = $v['faultString'];
+                    $r = new Response(0, $vc->scalarVal(), $vs->scalarVal());
+                } else {
+                    $r = new Response($v);
+                }
+                return $r;
+
+            case 'methodcall':
+                $req = new Request($_xh['method']);
+                for ($i = 0; $i < count($_xh['params']); $i++) {
+                    $req->addParam($_xh['params'][$i]);
+                }
+                return $req;
+
+            case 'value':
+                return $_xh['value'];
+
+            case 'fault':
+                // EPI api emulation
+                $v = $_xh['value'];
+                // use a known error code
+                /** @var Value $vc */
+                $vc = isset($v['faultCode']) ? $v['faultCode']->scalarVal() : PhpXmlRpc::$xmlrpcerr['invalid_return'];
+                /** @var Value $vs */
+                $vs = isset($v['faultString']) ? $v['faultString']->scalarVal() : '';
+                if (!is_int($vc) || $vc == 0) {
+                    $vc = PhpXmlRpc::$xmlrpcerr['invalid_return'];
+                }
+                return new Response(0, $vc, $vs);
+
+            default:
+                return false;
+        }
+    }
+}

+ 7 - 0
includes/phpxmlrpc/Exception.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace PhpXmlRpc;
+
+class Exception extends \Exception
+{
+}

+ 12 - 0
includes/phpxmlrpc/Exception/FaultResponseException.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace PhpXmlRpc\Exception;
+
+use PhpXmlRpc\Exception as BaseExtension;
+
+/**
+ * To be used when throwing exceptions instead of returning Response objects (future API?)
+ */
+class FaultResponseException extends BaseExtension
+{
+}

+ 22 - 0
includes/phpxmlrpc/Exception/HttpException.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace PhpXmlRpc\Exception;
+
+/**
+ * To be used for all errors related to parsing HTTP requests and responses
+ */
+class HttpException extends TransportException
+{
+    protected $statusCode;
+
+    public function __construct($message = "", $code = 0, $previous = null, $statusCode = null)
+    {
+        parent::__construct($message, $code, $previous);
+        $this->statusCode = $statusCode;
+    }
+
+    public function statusCode()
+    {
+        return $this->statusCode;
+    }
+}

+ 7 - 0
includes/phpxmlrpc/Exception/NoSuchMethodException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace PhpXmlRpc\Exception;
+
+class NoSuchMethodException extends ServerException
+{
+}

+ 12 - 0
includes/phpxmlrpc/Exception/ParsingException.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace PhpXmlRpc\Exception;
+
+use PhpXmlRpc\Exception as BaseExtension;
+
+/**
+ * Base exception for errors while parsing xml-rpc requests/responses: charset issue, xml issues, xml-rpc issues
+ */
+class ParsingException extends BaseExtension
+{
+}

+ 4 - 0
includes/phpxmlrpc/Exception/PhpXmlrpcException.php

@@ -0,0 +1,4 @@
+<?php
+
+// deprecated. Kept around for BC
+class_alias('PhpXmlRpc\Exception', 'PhpXmlRpc\Exception\PhpXmlRpcException');

+ 12 - 0
includes/phpxmlrpc/Exception/ServerException.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace PhpXmlRpc\Exception;
+
+use PhpXmlRpc\Exception as BaseExtension;
+
+/**
+ * Base exception for errors thrown by the server while trying to handle the requests, such as errors with the dispatch map
+ */
+class ServerException extends BaseExtension
+{
+}

+ 12 - 0
includes/phpxmlrpc/Exception/StateErrorException.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace PhpXmlRpc\Exception;
+
+use PhpXmlRpc\Exception as BaseExtension;
+
+/**
+ * Exception thrown when an object is in such a state that it can not fulfill execution of a method
+ */
+class StateErrorException extends BaseExtension
+{
+}

+ 12 - 0
includes/phpxmlrpc/Exception/TransportException.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace PhpXmlRpc\Exception;
+
+use PhpXmlRpc\Exception as BaseExtension;
+
+/**
+ * To be used for all errors related to the transport, which are not related to specifically to HTTP. Eg: can not open socket
+ */
+class TransportException extends BaseExtension
+{
+}

+ 12 - 0
includes/phpxmlrpc/Exception/TypeErrorException.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace PhpXmlRpc\Exception;
+
+use PhpXmlRpc\Exception as BaseExtension;
+
+/**
+ * Exception thrown when an argument passed to a function or method has an unsupported type
+ */
+class TypeErrorException extends BaseExtension
+{
+}

+ 12 - 0
includes/phpxmlrpc/Exception/ValueErrorException.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace PhpXmlRpc\Exception;
+
+use PhpXmlRpc\Exception as BaseExtension;
+
+/**
+ * Exception thrown when an argument passed to a function or method has an unsupported value (but its type is ok)
+ */
+class ValueErrorException extends BaseExtension
+{
+}

+ 7 - 0
includes/phpxmlrpc/Exception/XmlException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace PhpXmlRpc\Exception;
+
+class XmlException extends ParsingException
+{
+}

+ 7 - 0
includes/phpxmlrpc/Exception/XmlRpcException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace PhpXmlRpc\Exception;
+
+class XmlRpcException extends ParsingException
+{
+}

+ 395 - 0
includes/phpxmlrpc/Helper/Charset.php

@@ -0,0 +1,395 @@
+<?php
+
+namespace PhpXmlRpc\Helper;
+
+use PhpXmlRpc\Exception\ValueErrorException;
+use PhpXmlRpc\PhpXmlRpc;
+use PhpXmlRpc\Traits\DeprecationLogger;
+
+/**
+ * @todo implement an interface
+ */
+class Charset
+{
+    use DeprecationLogger;
+
+    // tables used for transcoding different charsets into us-ascii xml
+    protected $xml_iso88591_Entities = array("in" => array(), "out" => array());
+
+    //protected $xml_cp1252_Entities = array('in' => array(), out' => array());
+
+    protected $charset_supersets = array(
+        'US-ASCII' => array('ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3', 'ISO-8859-4',
+            'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8',
+            'ISO-8859-9', 'ISO-8859-10', 'ISO-8859-11', 'ISO-8859-12',
+            'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'UTF-8',
+            'EUC-JP', 'EUC-', 'EUC-KR', 'EUC-CN',),
+    );
+
+    /** @var Charset $instance */
+    protected static $instance = null;
+
+    /**
+     * This class is singleton for performance reasons.
+     *
+     * @return Charset
+     *
+     * @todo should we just make $xml_iso88591_Entities a static variable instead ?
+     */
+    public static function instance()
+    {
+        if (self::$instance === null) {
+            self::$instance = new static();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * Force usage as singleton.
+     */
+    protected function __construct()
+    {
+    }
+
+    /**
+     * @param string $tableName
+     * @return void
+     * @throws ValueErrorException for unsupported $tableName
+     *
+     * @todo add support for cp1252 as well as latin-2 .. latin-10
+     *       Optimization creep: instead of building all those tables on load, keep them ready-made php files
+     *       which are not even included until needed
+     * @todo should we add to the latin-1 table the characters from cp_1252 range, i.e. 128 to 159 ?
+     *       Those will NOT be present in true ISO-8859-1, but will save the unwary windows user from sending junk
+     *       (though no luck when receiving them...)
+     *       Note also that, apparently, while 'ISO/IEC 8859-1' has no characters defined for bytes 128 to 159,
+     *       IANA ISO-8859-1 does have well-defined 'C1' control codes for those - wikipedia's page on latin-1 says:
+     *       "ISO-8859-1 is the IANA preferred name for this standard when supplemented with the C0 and C1 control codes
+     *       from ISO/IEC 6429." Check what mbstring/iconv do by default with those?
+     */
+    protected function buildConversionTable($tableName)
+    {
+        switch ($tableName) {
+            case 'xml_iso88591_Entities':
+                if (count($this->xml_iso88591_Entities['in'])) {
+                    return;
+                }
+                for ($i = 0; $i < 32; $i++) {
+                    $this->xml_iso88591_Entities["in"][] = chr($i);
+                    $this->xml_iso88591_Entities["out"][] = "&#{$i};";
+                }
+
+                /// @todo to be 'print safe', should we encode as well character 127 (DEL) ?
+
+                for ($i = 160; $i < 256; $i++) {
+                    $this->xml_iso88591_Entities["in"][] = chr($i);
+                    $this->xml_iso88591_Entities["out"][] = "&#{$i};";
+                }
+                break;
+
+            /*case 'xml_cp1252_Entities':
+                if (count($this->xml_cp1252_Entities['in'])) {
+                    return;
+                }
+                for ($i = 128; $i < 160; $i++)
+                {
+                    $this->xml_cp1252_Entities['in'][] = chr($i);
+                }
+                $this->xml_cp1252_Entities['out'] = array(
+                    '&#x20AC;', '?',        '&#x201A;', '&#x0192;',
+                    '&#x201E;', '&#x2026;', '&#x2020;', '&#x2021;',
+                    '&#x02C6;', '&#x2030;', '&#x0160;', '&#x2039;',
+                    '&#x0152;', '?',        '&#x017D;', '?',
+                    '?',        '&#x2018;', '&#x2019;', '&#x201C;',
+                    '&#x201D;', '&#x2022;', '&#x2013;', '&#x2014;',
+                    '&#x02DC;', '&#x2122;', '&#x0161;', '&#x203A;',
+                    '&#x0153;', '?',        '&#x017E;', '&#x0178;'
+                );
+                $this->buildConversionTable('xml_iso88591_Entities');
+                break;*/
+
+            default:
+                throw new ValueErrorException('Unsupported table: ' . $tableName);
+        }
+    }
+
+    /**
+     * Convert a string to the correct XML representation in a target charset.
+     * This involves:
+     * - character transformation for all characters which have a different representation in source and dest charsets
+     * - using 'charset entity' representation for all characters which are outside the target charset
+     *
+     * To help correct communication of non-ascii chars inside strings, regardless of the charset used when sending
+     * requests, parsing them, sending responses and parsing responses, an option is to convert all non-ascii chars
+     * present in the message into their equivalent 'charset entity'. Charset entities enumerated this way are
+     * independent of the charset encoding used to transmit them, and all XML parsers are bound to understand them.
+     *
+     * Note that when not sending a charset encoding mime type along with http headers, we are bound by RFC 3023 to emit
+     * strict us-ascii for 'text/xml' payloads (but we should review RFC 7303, which seems to have changed the rules...)
+     *
+     * @param string $data
+     * @param string $srcEncoding
+     * @param string $destEncoding
+     * @return string
+     *
+     * @todo do a bit of basic benchmarking: strtr vs. str_replace, str_replace vs htmlspecialchars, hand-coded conversion
+     *       vs mbstring when that is enabled
+     * @todo make use of iconv when it is available and mbstring is not
+     * @todo support aliases for charset names, eg ASCII, LATIN1, ISO-88591 (see f.e. polyfill-iconv for a list),
+     *       but then take those into account as well in other methods, ie. isValidCharset)
+     * @todo when converting to ASCII, allow to choose whether to escape the range 0-31,127 (non-print chars) or not
+     * @todo allow picking different strategies to deal w. invalid chars? eg. source in latin-1 and chars 128-159
+     * @todo add support for escaping using CDATA sections? (add cdata start and end tokens, replace only ']]>' with ']]]]><![CDATA[>')
+     */
+    public function encodeEntities($data, $srcEncoding = '', $destEncoding = '')
+    {
+        if ($srcEncoding == '') {
+            // lame, but we know no better...
+            $srcEncoding = PhpXmlRpc::$xmlrpc_internalencoding;
+        }
+
+        if ($destEncoding == '') {
+            $destEncoding = 'US-ASCII';
+        }
+
+        // in case there is transcoding going on, let's upscale to UTF8
+        /// @todo we should do this as well when $srcEncoding == $destEncoding and the encoding is not supported by
+        ///       htmlspecialchars
+        if (!in_array($srcEncoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII')) && $srcEncoding != $destEncoding &&
+            function_exists('mb_convert_encoding')) {
+            $data = mb_convert_encoding($data, 'UTF-8', str_replace('US-ASCII', 'ASCII', $srcEncoding));
+            $srcEncoding = 'UTF-8';
+        }
+
+        $conversion = strtoupper($srcEncoding . '_' . $destEncoding);
+
+        // list ordered with (expected) most common scenarios first
+        switch ($conversion) {
+            case 'UTF-8_UTF-8':
+            case 'ISO-8859-1_ISO-8859-1':
+            case 'US-ASCII_UTF-8':
+            case 'US-ASCII_US-ASCII':
+            case 'US-ASCII_ISO-8859-1':
+            //case 'CP1252_CP1252':
+                $escapedData = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
+                break;
+
+            case 'UTF-8_US-ASCII':
+            case 'UTF-8_ISO-8859-1':
+                // NB: this will choke on invalid UTF-8, going most likely beyond EOF
+                $escapedData = '';
+                // be kind to users creating string xml-rpc values out of different php types
+                $data = (string)$data;
+                $ns = strlen($data);
+                for ($nn = 0; $nn < $ns; $nn++) {
+                    $ch = $data[$nn];
+                    $ii = ord($ch);
+                    // 7 bits in 1 byte: 0bbbbbbb (127)
+                    if ($ii < 32) {
+                        if ($conversion == 'UTF-8_US-ASCII') {
+                            $escapedData .= sprintf('&#%d;', $ii);
+                        } else {
+                            $escapedData .= $ch;
+                        }
+                    }
+                    else if ($ii < 128) {
+                        /// @todo shall we replace this with a (supposedly) faster str_replace?
+                        /// @todo to be 'print safe', should we encode as well character 127 (DEL) ?
+                        switch ($ii) {
+                            case 34:
+                                $escapedData .= '&quot;';
+                                break;
+                            case 38:
+                                $escapedData .= '&amp;';
+                                break;
+                            case 39:
+                                $escapedData .= '&apos;';
+                                break;
+                            case 60:
+                                $escapedData .= '&lt;';
+                                break;
+                            case 62:
+                                $escapedData .= '&gt;';
+                                break;
+                            default:
+                                $escapedData .= $ch;
+                        } // switch
+                    } // 11 bits in 2 bytes: 110bbbbb 10bbbbbb (2047)
+                    elseif ($ii >> 5 == 6) {
+                        $b1 = ($ii & 31);
+                        $b2 = (ord($data[$nn + 1]) & 63);
+                        $ii = ($b1 * 64) + $b2;
+                        $escapedData .= sprintf('&#%d;', $ii);
+                        $nn += 1;
+                    } // 16 bits in 3 bytes: 1110bbbb 10bbbbbb 10bbbbbb
+                    elseif ($ii >> 4 == 14) {
+                        $b1 = ($ii & 15);
+                        $b2 = (ord($data[$nn + 1]) & 63);
+                        $b3 = (ord($data[$nn + 2]) & 63);
+                        $ii = ((($b1 * 64) + $b2) * 64) + $b3;
+                        $escapedData .= sprintf('&#%d;', $ii);
+                        $nn += 2;
+                    } // 21 bits in 4 bytes: 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb
+                    elseif ($ii >> 3 == 30) {
+                        $b1 = ($ii & 7);
+                        $b2 = (ord($data[$nn + 1]) & 63);
+                        $b3 = (ord($data[$nn + 2]) & 63);
+                        $b4 = (ord($data[$nn + 3]) & 63);
+                        $ii = ((((($b1 * 64) + $b2) * 64) + $b3) * 64) + $b4;
+                        $escapedData .= sprintf('&#%d;', $ii);
+                        $nn += 3;
+                    }
+                }
+
+                // when converting to latin-1, do not be so eager with using entities for characters 160-255
+                if ($conversion == 'UTF-8_ISO-8859-1') {
+                    $this->buildConversionTable('xml_iso88591_Entities');
+                    $escapedData = str_replace(array_slice($this->xml_iso88591_Entities['out'], 32), array_slice($this->xml_iso88591_Entities['in'], 32), $escapedData);
+                }
+                break;
+
+            case 'ISO-8859-1_UTF-8':
+                $escapedData = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
+                /// @todo if on php >= 8.2, prefer using mbstring or iconv. Also: suppress the warning!
+                if (function_exists('mb_convert_encoding')) {
+                        $escapedData = mb_convert_encoding($escapedData, 'UTF-8', 'ISO-8859-1');
+                } else {
+                    $escapedData = utf8_encode($escapedData);
+                }
+                break;
+
+            case 'ISO-8859-1_US-ASCII':
+                $this->buildConversionTable('xml_iso88591_Entities');
+                $escapedData = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
+                $escapedData = str_replace($this->xml_iso88591_Entities['in'], $this->xml_iso88591_Entities['out'], $escapedData);
+                break;
+
+            /*
+            case 'CP1252_US-ASCII':
+                $this->buildConversionTable('xml_cp1252_Entities');
+                $escapedData = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
+                $escapedData = str_replace($this->xml_iso88591_Entities']['in'], $this->xml_iso88591_Entities['out'], $escapedData);
+                $escapedData = str_replace($this->xml_cp1252_Entities['in'], $this->xml_cp1252_Entities['out'], $escapedData);
+                break;
+            case 'CP1252_UTF-8':
+                $this->buildConversionTable('xml_cp1252_Entities');
+                $escapedData = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
+                /// @todo we could use real UTF8 chars here instead of xml entities... (note that utf_8 encode all alone will NOT convert them)
+                $escapedData = str_replace($this->xml_cp1252_Entities['in'], $this->xml_cp1252_Entities['out'], $escapedData);
+                $escapedData = utf8_encode($escapedData);
+                break;
+            case 'CP1252_ISO-8859-1':
+                $this->buildConversionTable('xml_cp1252_Entities');
+                $escapedData = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
+                // we might as well replace all funky chars with a '?' here, but we are kind and leave it to the receiving application layer to decide what to do with these weird entities...
+                $escapedData = str_replace($this->xml_cp1252_Entities['in'], $this->xml_cp1252_Entities['out'], $escapedData);
+                break;
+            */
+
+            default:
+                if (function_exists('mb_convert_encoding')) {
+                    // If reaching where, there are only 2 cases possible: UTF8->XXX or XXX->XXX
+                    // If src is UTF8, we run htmlspecialchars before converting to the target charset, as
+                    // htmlspecialchars has limited charset support, but it groks utf8
+                    if ($srcEncoding === 'UTF-8') {
+                        $data = htmlspecialchars($data,  defined('ENT_XML1') ? ENT_XML1 | ENT_QUOTES : ENT_QUOTES, 'UTF-8');
+                    }
+                    if ($srcEncoding !== $destEncoding) {
+                        try {
+                            // php 7.4 and lower: a warning is generated. php 8.0 and up: an Error is thrown. So much for BC...
+                            $data = @mb_convert_encoding($data, str_replace('US-ASCII', 'ASCII', $destEncoding), str_replace('US-ASCII', 'ASCII', $srcEncoding));
+                        } catch (\ValueError $e) {
+                            $data = false;
+                        }
+                    }
+                    if ($data === false) {
+                        $escapedData = '';
+                        $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ": Converting from $srcEncoding to $destEncoding via mbstring: failed...");
+                    } else {
+                        if ($srcEncoding === 'UTF-8') {
+                            $escapedData = $data;
+                        } else {
+                            $escapedData = htmlspecialchars($data, defined('ENT_XML1') ? ENT_XML1 | ENT_QUOTES : ENT_QUOTES, $destEncoding);
+                        }
+                    }
+                } else {
+                    $escapedData = '';
+                    $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ": Converting from $srcEncoding to $destEncoding: not supported...");
+                }
+        }
+
+        return $escapedData;
+    }
+
+    /**
+     * @return string[]
+     */
+    public function knownCharsets()
+    {
+        $knownCharsets = array('UTF-8', 'ISO-8859-1', 'US-ASCII');
+        // Add all charsets which mbstring can handle, but remove junk not found in IANA registry at
+        // http://www.iana.org/assignments/character-sets/character-sets.xhtml
+        if (function_exists('mb_list_encodings')) {
+            $knownCharsets = array_unique(array_merge($knownCharsets, array_diff(mb_list_encodings(), array(
+                'pass', 'auto', 'wchar', 'BASE64', 'UUENCODE', 'ASCII', 'HTML-ENTITIES', 'Quoted-Printable',
+                '7bit','8bit', 'byte2be', 'byte2le', 'byte4be', 'byte4le'
+            ))));
+        }
+        return $knownCharsets;
+    }
+
+    // *** BC layer ***
+
+    /**
+     * Checks if a given charset encoding is present in a list of encodings or if it is a valid subset of any encoding
+     * in the list.
+     * @deprecated kept around for BC, as it is not in use by the lib
+     *
+     * @param string $encoding charset to be tested
+     * @param string|array $validList comma separated list of valid charsets (or array of charsets)
+     * @return bool
+     */
+    public function isValidCharset($encoding, $validList)
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        if (is_string($validList)) {
+            $validList = explode(',', $validList);
+        }
+        if (in_array(strtoupper($encoding), $validList)) {
+            return true;
+        } else {
+            if (array_key_exists($encoding, $this->charset_supersets)) {
+                foreach ($validList as $allowed) {
+                    if (in_array($allowed, $this->charset_supersets[$encoding])) {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+    }
+
+    /**
+     * Used only for backwards compatibility (the .inc shims).
+     * @deprecated
+     *
+     * @param string $charset
+     * @return array
+     * @throws ValueErrorException for unknown/unsupported charsets
+     */
+    public function getEntities($charset)
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        switch ($charset)
+        {
+            case 'iso88591':
+                return $this->xml_iso88591_Entities;
+            default:
+                throw new ValueErrorException('Unsupported charset: ' . $charset);
+        }
+    }
+}

+ 64 - 0
includes/phpxmlrpc/Helper/Date.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace PhpXmlRpc\Helper;
+
+use PhpXmlRpc\PhpXmlRpc;
+
+/**
+ * Helps to convert timestamps to the xml-rpc date format.
+ *
+ * Feature creep -- add support for custom TZs
+ */
+class Date
+{
+    /**
+     * Given a timestamp, return the corresponding ISO8601 encoded string.
+     *
+     * Really, timezones ought to be supported but the XML-RPC spec says:
+     *
+     * "Don't assume a timezone. It should be specified by the server in its documentation what assumptions it makes
+     *  about timezones."
+     *
+     * This routine always encodes to local time unless $utc is set to 1, in which case UTC output is produced and an
+     * adjustment for the local timezone's offset is made
+     *
+     * @param int|\DateTimeInterface $timet timestamp or datetime
+     * @param bool|int $utc (0 or 1)
+     * @return string
+     */
+    public static function iso8601Encode($timet, $utc = 0)
+    {
+        if (is_a($timet, 'DateTimeInterface') || is_a($timet, 'DateTime')) {
+            $timet = $timet->getTimestamp();
+        }
+        if (!$utc) {
+            $t = date('Ymd\TH:i:s', $timet);
+        } else {
+            $t = gmdate('Ymd\TH:i:s', $timet);
+        }
+
+        return $t;
+    }
+
+    /**
+     * Given an ISO8601 date string, return a timestamp in the localtime, or UTC.
+     *
+     * @param string $idate
+     * @param bool|int $utc either 0 (assume date is in local time) or 1 (assume date is in UTC)
+     *
+     * @return int (timestamp) 0 if the source string does not match the xml-rpc dateTime format
+     */
+    public static function iso8601Decode($idate, $utc = 0)
+    {
+        $t = 0;
+        if (preg_match(PhpXmlRpc::$xmlrpc_datetime_format, $idate, $regs)) {
+            if ($utc) {
+                $t = gmmktime($regs[4], $regs[5], $regs[6], $regs[2], $regs[3], $regs[1]);
+            } else {
+                $t = mktime($regs[4], $regs[5], $regs[6], $regs[2], $regs[3], $regs[1]);
+            }
+        }
+
+        return $t;
+    }
+}

+ 283 - 0
includes/phpxmlrpc/Helper/Http.php

@@ -0,0 +1,283 @@
+<?php
+
+namespace PhpXmlRpc\Helper;
+
+use PhpXmlRpc\Exception\HttpException;
+use PhpXmlRpc\PhpXmlRpc;
+use PhpXmlRpc\Traits\LoggerAware;
+
+class Http
+{
+    use LoggerAware;
+
+    /**
+     * Decode a string that is encoded with "chunked" transfer encoding as defined in RFC 2068 par. 19.4.6.
+     * Code shamelessly stolen from nusoap library by Dietrich Ayala.
+     * @internal this function will become protected in the future
+     *
+     * @param string $buffer the string to be decoded
+     * @return string
+     */
+    public static function decodeChunked($buffer)
+    {
+        // length := 0
+        $length = 0;
+        $new = '';
+
+        // read chunk-size, chunk-extension (if any) and crlf
+        // get the position of the linebreak
+        $chunkEnd = strpos($buffer, "\r\n") + 2;
+        $temp = substr($buffer, 0, $chunkEnd);
+        $chunkSize = hexdec(trim($temp));
+        $chunkStart = $chunkEnd;
+        while ($chunkSize > 0) {
+            $chunkEnd = strpos($buffer, "\r\n", $chunkStart + $chunkSize);
+
+            // just in case we got a broken connection
+            if ($chunkEnd == false) {
+                $chunk = substr($buffer, $chunkStart);
+                // append chunk-data to entity-body
+                $new .= $chunk;
+                $length += strlen($chunk);
+                break;
+            }
+
+            // read chunk-data and crlf
+            $chunk = substr($buffer, $chunkStart, $chunkEnd - $chunkStart);
+            // append chunk-data to entity-body
+            $new .= $chunk;
+            // length := length + chunk-size
+            $length += strlen($chunk);
+            // read chunk-size and crlf
+            $chunkStart = $chunkEnd + 2;
+
+            $chunkEnd = strpos($buffer, "\r\n", $chunkStart) + 2;
+            if ($chunkEnd == false) {
+                break; // just in case we got a broken connection
+            }
+            $temp = substr($buffer, $chunkStart, $chunkEnd - $chunkStart);
+            $chunkSize = hexdec(trim($temp));
+            $chunkStart = $chunkEnd;
+        }
+
+        return $new;
+    }
+
+    /**
+     * Parses HTTP an http response's headers and separates them from the body.
+     *
+     * @param string $data the http response, headers and body. It will be stripped of headers
+     * @param bool $headersProcessed when true, we assume that response inflating and dechunking has been already carried out
+     * @param int $debug when > 0, logs to screen messages detailing info about the parsed data
+     * @return array with keys 'headers', 'cookies', 'raw_data' and 'status_code'
+     * @throws HttpException
+     *
+     * @todo if $debug is < 0, we could avoid populating 'raw_data' in the returned value - but that would
+     *       be a weird API... (note that we still need to always have headers parsed for content charset)
+     */
+    public function parseResponseHeaders(&$data, $headersProcessed = false, $debug = 0)
+    {
+        $httpResponse = array('raw_data' => $data, 'headers'=> array(), 'cookies' => array(), 'status_code' => null);
+
+        // Support "web-proxy-tunnelling" connections for https through proxies
+        if (preg_match('/^HTTP\/1\.[0-1] 200 Connection established/', $data)) {
+            // Look for CR/LF or simple LF as line separator (even though it is not valid http)
+            $pos = strpos($data, "\r\n\r\n");
+            if ($pos || is_int($pos)) {
+                $bd = $pos + 4;
+            } else {
+                $pos = strpos($data, "\n\n");
+                if ($pos || is_int($pos)) {
+                    $bd = $pos + 2;
+                } else {
+                    // No separation between response headers and body: fault?
+                    $bd = 0;
+                }
+            }
+            if ($bd) {
+                // this filters out all http headers from proxy. maybe we could take them into account, too?
+                $data = substr($data, $bd);
+            } else {
+                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': HTTPS via proxy error, tunnel connection possibly failed');
+                throw new HttpException(PhpXmlRpc::$xmlrpcstr['http_error'] . ' (HTTPS via proxy error, tunnel connection possibly failed)', PhpXmlRpc::$xmlrpcerr['http_error']);
+            }
+        }
+
+        // Strip HTTP 1.1 100 Continue header if present
+        while (preg_match('/^HTTP\/1\.1 1[0-9]{2} /', $data)) {
+            $pos = strpos($data, 'HTTP', 12);
+            // server sent a Continue header without any (valid) content following...
+            // give the client a chance to know it
+            if (!$pos && !is_int($pos)) {
+                /// @todo this construct works fine in php 3, 4 and 5 - 8; would it not be enough to have !== false now ?
+
+                break;
+            }
+            $data = substr($data, $pos);
+        }
+
+        // When using Curl to query servers using Digest Auth, we get back a double set of http headers.
+        // Same when following redirects
+        // We strip out the 1st...
+        /// @todo we should let the caller know that there was a redirect involved
+        if ($headersProcessed && preg_match('/^HTTP\/[0-9](?:\.[0-9])? (?:401|30[1278]) /', $data)) {
+            if (preg_match('/(\r?\n){2}HTTP\/[0-9](?:\.[0-9])? 200 /', $data)) {
+                $data = preg_replace('/^HTTP\/[0-9](?:\.[0-9])? (?:401|30[1278]) .+?(?:\r?\n){2}(HTTP\/[0-9.]+ 200 )/s', '$1', $data, 1);
+            }
+        }
+
+        if (preg_match('/^HTTP\/([0-9](?:\.[0-9])?) ([0-9]{3}) /', $data, $matches)) {
+            $httpResponse['protocol_version'] = $matches[1];
+            $httpResponse['status_code'] = $matches[2];
+        }
+
+        if ($httpResponse['status_code'] !== '200') {
+            $errstr = substr($data, 0, strpos($data, "\n") - 1);
+            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': HTTP error, got response: ' . $errstr);
+            throw new HttpException(PhpXmlRpc::$xmlrpcstr['http_error'] . ' (' . $errstr . ')', PhpXmlRpc::$xmlrpcerr['http_error'], null, $httpResponse['status_code']);
+        }
+
+        // be tolerant to usage of \n instead of \r\n to separate headers and data (even though it is not valid http)
+        $pos = strpos($data, "\r\n\r\n");
+        if ($pos || is_int($pos)) {
+            $bd = $pos + 4;
+        } else {
+            $pos = strpos($data, "\n\n");
+            if ($pos || is_int($pos)) {
+                $bd = $pos + 2;
+            } else {
+                // No separation between response headers and body: fault?
+                // we could take some action here instead of going on...
+                $bd = 0;
+            }
+        }
+
+        // be tolerant to line endings, and extra empty lines
+        $ar = preg_split("/\r?\n/", trim(substr($data, 0, $pos)));
+
+        foreach ($ar as $line) {
+            // take care of (multi-line) headers and cookies
+            $arr = explode(':', $line, 2);
+            if (count($arr) > 1) {
+                /// @todo according to https://www.rfc-editor.org/rfc/rfc7230#section-3.2.4, we should reject with error
+                ///       400 any messages where a space is present between the header name and colon
+                $headerName = strtolower(trim($arr[0]));
+                if ($headerName == 'set-cookie') {
+                    $cookie = $arr[1];
+                    // glue together all received cookies, using a comma to separate them (same as php does with getallheaders())
+                    if (isset($httpResponse['headers'][$headerName])) {
+                        $httpResponse['headers'][$headerName] .= ', ' . trim($cookie);
+                    } else {
+                        $httpResponse['headers'][$headerName] = trim($cookie);
+                    }
+                    // parse cookie attributes, in case user wants to correctly honour them
+                    // @todo support for server sending multiple time cookie with same name, but using different PATHs
+                    $cookie = explode(';', $cookie);
+                    foreach ($cookie as $pos => $val) {
+                        $val = explode('=', $val, 2);
+                        $tag = trim($val[0]);
+                        $val = isset($val[1]) ? trim($val[1]) : '';
+                        if ($pos === 0) {
+                            $cookieName = $tag;
+                            // if present, we have strip leading and trailing " chars from $val
+                            if (preg_match('/^"(.*)"$/', $val, $matches)) {
+                                $val = $matches[1];
+                            }
+                            $httpResponse['cookies'][$cookieName] = array('value' => urldecode($val));
+                        } else {
+                            $httpResponse['cookies'][$cookieName][$tag] = $val;
+                        }
+                    }
+                } else {
+                    /// @todo some other headers (the ones that allow a CSV list of values) do allow many values to be
+                    ///       passed using multiple header lines.
+                    ///       We should add content to $xmlrpc->_xh['headers'][$headerName] instead of replacing it for those...
+                    $httpResponse['headers'][$headerName] = trim($arr[1]);
+                }
+            } elseif (isset($headerName)) {
+                /// @todo improve this: 1. check that the line starts with a space or tab; 2. according to
+                ///       https://www.rfc-editor.org/rfc/rfc7230#section-3.2.4, we should flat out refuse these messages
+                $httpResponse['headers'][$headerName] .= ' ' . trim($line);
+            }
+        }
+
+        $data = substr($data, $bd);
+
+        if ($debug && count($httpResponse['headers'])) {
+            $msg = '';
+            foreach ($httpResponse['headers'] as $header => $value) {
+                $msg .= "HEADER: $header: $value\n";
+            }
+            foreach ($httpResponse['cookies'] as $header => $value) {
+                $msg .= "COOKIE: $header={$value['value']}\n";
+            }
+            $this->getLogger()->debug($msg);
+        }
+
+        // if CURL was used for the call, http headers have been processed, and dechunking + reinflating have been carried out
+        if (!$headersProcessed) {
+
+            // Decode chunked encoding sent by http 1.1 servers
+            if (isset($httpResponse['headers']['transfer-encoding']) && $httpResponse['headers']['transfer-encoding'] == 'chunked') {
+                if (!$data = static::decodeChunked($data)) {
+                    $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': errors occurred when trying to rebuild the chunked data received from server');
+                    throw new HttpException(PhpXmlRpc::$xmlrpcstr['dechunk_fail'], PhpXmlRpc::$xmlrpcerr['dechunk_fail'], null, $httpResponse['status_code']);
+                }
+            }
+
+            // Decode gzip-compressed stuff
+            // code shamelessly inspired from nusoap library by Dietrich Ayala
+            if (isset($httpResponse['headers']['content-encoding'])) {
+                $httpResponse['headers']['content-encoding'] = str_replace('x-', '', $httpResponse['headers']['content-encoding']);
+                if ($httpResponse['headers']['content-encoding'] == 'deflate' || $httpResponse['headers']['content-encoding'] == 'gzip') {
+                    // if decoding works, use it. else assume data wasn't gzencoded
+                    if (function_exists('gzinflate')) {
+                        if ($httpResponse['headers']['content-encoding'] == 'deflate' && $degzdata = @gzuncompress($data)) {
+                            $data = $degzdata;
+                            if ($debug) {
+                                $this->getLogger()->debug("---INFLATED RESPONSE---[" . strlen($data) . " chars]---\n$data\n---END---");
+                            }
+                        } elseif ($httpResponse['headers']['content-encoding'] == 'gzip' && $degzdata = @gzinflate(substr($data, 10))) {
+                            $data = $degzdata;
+                            if ($debug) {
+                                $this->getLogger()->debug("---INFLATED RESPONSE---[" . strlen($data) . " chars]---\n$data\n---END---");
+                            }
+                        } else {
+                            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': errors occurred when trying to decode the deflated data received from server');
+                            throw new HttpException(PhpXmlRpc::$xmlrpcstr['decompress_fail'], PhpXmlRpc::$xmlrpcerr['decompress_fail'], null, $httpResponse['status_code']);
+                        }
+                    } else {
+                        $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': the server sent deflated data. Your php install must have the Zlib extension compiled in to support this.');
+                        throw new HttpException(PhpXmlRpc::$xmlrpcstr['cannot_decompress'], PhpXmlRpc::$xmlrpcerr['cannot_decompress'], null, $httpResponse['status_code']);
+                    }
+                }
+            }
+        } // end of 'if needed, de-chunk, re-inflate response'
+
+        return $httpResponse;
+    }
+
+    /**
+     * Parses one of the http headers which can have a list of values with quality param.
+     * @see https://www.rfc-editor.org/rfc/rfc7231#section-5.3.1
+     *
+     * @param string $header
+     * @return string[]
+     */
+    public function parseAcceptHeader($header)
+    {
+        $accepted = array();
+        foreach(explode(',', $header) as $c) {
+            if (preg_match('/^([^;]+); *q=([0-9.]+)/', $c, $matches)) {
+                $c = $matches[1];
+                $w = $matches[2];
+            } else {
+                $c = preg_replace('/;.*/', '', $c);
+                $w = 1;
+            }
+            $accepted[(trim($c))] = $w;
+        }
+        arsort($accepted);
+        return array_keys($accepted);
+    }
+}

+ 42 - 0
includes/phpxmlrpc/Helper/Interop.php

@@ -0,0 +1,42 @@
+<?php
+
+namespace PhpXmlRpc\Helper;
+
+/**
+ * A helper dedicated to support Interoperability features
+ */
+class Interop
+{
+    /// @todo review - should we use the range -32099 .. -32000 for some server erors?
+    public static $xmlrpcerr = array(
+        'unknown_method' => -32601,
+        'invalid_return' => 2,
+        'incorrect_params' => -32602,
+        'introspect_unknown' => -32601, // this shares the same code but has a separate meaning from 'unknown_method'...
+        'http_error' => -32300,
+        'no_data' => -32700,
+        'no_ssl' => -32400,
+        'curl_fail' => -32400,
+        'invalid_request' => -32600,
+        'no_curl' => -32400,
+        'server_error' => -32500,
+        'multicall_error' => -32700,
+        'multicall_notstruct' => -32600,
+        'multicall_nomethod' => -32601,
+        'multicall_notstring' => -32600,
+        'multicall_recursion' => -32603,
+        'multicall_noparams' => -32602,
+        'multicall_notarray' => -32600,
+        'no_http2' => -32400,
+        'invalid_xml' => -32700,
+        'xml_not_compliant' => -32700,
+        'xml_parsing_error' => -32700,
+        'cannot_decompress' => -32400,
+        'decompress_fail' => -32300,
+        'dechunk_fail' => -32300,
+        'server_cannot_decompress' => -32300,
+        'server_decompress_fail' => -32300,
+    );
+
+    public static $xmlrpcerruser = -32000;
+}

+ 123 - 0
includes/phpxmlrpc/Helper/Logger.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace PhpXmlRpc\Helper;
+
+/**
+ * @todo implement an interface
+ * @todo make constructor private to force users to go through `instance()` ?
+ */
+class Logger
+{
+    protected static $instance = null;
+
+    /**
+     * This class can be used as singleton, so that later we can move to DI patterns (ish...)
+     *
+     * @return Logger
+     */
+    public static function instance()
+    {
+        if (self::$instance === null) {
+            self::$instance = new self();
+        }
+
+        return self::$instance;
+    }
+
+    // *** Implement the same interface as PSR/LOG, for the sake of interoperability ***
+
+    /**
+     * NB: unlike other "traditional" loggers, this one echoes to screen the debug messages instead of logging them.
+     *
+     * @param string $message
+     * @param array $context known key: 'encoding'
+     * @return void
+     */
+    public function debug($message, $context = array())
+    {
+        if (isset($context['encoding'])) {
+            $this->debugMessage($message, $context['encoding']);
+        } else {
+            $this->debugMessage($message);
+        }
+    }
+
+    /**
+     * Following the general principle of 'never break stdout', the default behaviour
+     *
+     * @param string $message
+     * @param $context
+     * @return void
+     */
+    public function warning($message, $context = array())
+    {
+        $this->errorLog(preg_replace('/^XML-RPC :/', 'XML-RPC Warning: ', $message));
+    }
+
+    /**
+     * Triggers the writing of a message to php's error log
+     *
+     * @param string $message
+     * @param array $context
+     * @return void
+     */
+    public function error($message, $context = array())
+    {
+        $this->errorLog(preg_replace('/^XML-RPC :/', 'XML-RPC Error: ', $message));
+    }
+
+    // BC interface
+
+    /**
+     * Echoes a debug message, taking care of escaping it when not in console mode.
+     * NB: if the encoding of the message is not known or wrong, and we are working in web mode, there is no guarantee
+     *     of 100% accuracy, which kind of defeats the purpose of debugging
+     *
+     * @param string $message
+     * @param string $encoding deprecated
+     * @return void
+     *
+     * @internal left in purely for BC
+     */
+    public function debugMessage($message, $encoding = null)
+    {
+        // US-ASCII is a warning for PHP and a fatal for HHVM
+        if ($encoding == 'US-ASCII') {
+            $encoding = 'UTF-8';
+        }
+
+        if (PHP_SAPI != 'cli') {
+            $flags = ENT_COMPAT;
+            // avoid warnings on php < 5.4...
+            if (defined('ENT_HTML401')) {
+                $flags =  $flags | ENT_HTML401;
+            }
+            if (defined('ENT_SUBSTITUTE')) {
+                $flags =  $flags | ENT_SUBSTITUTE;
+            }
+            if ($encoding != null) {
+                print "<PRE>\n".htmlentities($message, $flags, $encoding)."\n</PRE>";
+            } else {
+                print "<PRE>\n".htmlentities($message, $flags)."\n</PRE>";
+            }
+        } else {
+            print "\n$message\n";
+        }
+
+        // let the user see this now in case there's a time-out later...
+        flush();
+    }
+
+    /**
+     * Writes a message to the error log.
+     *
+     * @param string $message
+     * @return void
+     *
+     * @internal left in purely for BC
+     */
+    public function errorLog($message)
+    {
+        error_log($message);
+    }
+}

+ 1098 - 0
includes/phpxmlrpc/Helper/XMLParser.php

@@ -0,0 +1,1098 @@
+<?php
+
+namespace PhpXmlRpc\Helper;
+
+use PhpXmlRpc\PhpXmlRpc;
+use PhpXmlRpc\Traits\DeprecationLogger;
+use PhpXmlRpc\Value;
+
+/**
+ * Deals with parsing the XML.
+ * @see http://xmlrpc.com/spec.md
+ *
+ * @todo implement an interface to allow for alternative implementations
+ *       - make access to $_xh protected, return more high-level data structures
+ *       - move the private parts of $_xh to the internal-use parsing-options config
+ *       - add parseRequest, parseResponse, parseValue methods
+ * @todo if iconv() or mb_string() are available, we could allow to convert the received xml to a custom charset encoding
+ *       while parsing, which is faster than doing it later by going over the rebuilt data structure
+ * @todo rename? This is an xml-rpc parser, not a generic xml parser...
+ *
+ * @property array $xmlrpc_valid_parents deprecated - public access left in purely for BC
+ * @property int $accept deprecated - (protected) access left in purely for BC
+ */
+class XMLParser
+{
+    use DeprecationLogger;
+
+    const RETURN_XMLRPCVALS = 'xmlrpcvals';
+    const RETURN_EPIVALS = 'epivals';
+    const RETURN_PHP = 'phpvals';
+
+    const ACCEPT_REQUEST = 1;
+    const ACCEPT_RESPONSE = 2;
+    const ACCEPT_VALUE = 4;
+    const ACCEPT_FAULT = 8;
+
+    /**
+     * @var int
+     * The max length beyond which data will get truncated in error messages
+     */
+    protected $maxLogValueLength = 100;
+
+    /**
+     * @var array
+     * Used to store state during parsing and to pass parsing results to callers.
+     * Quick explanation of components:
+     *  private:
+     *    ac - used to accumulate values
+     *    stack - array with genealogy of xml elements names, used to validate nesting of xml-rpc elements
+     *    valuestack - array used for parsing arrays and structs
+     *    lv - used to indicate "looking for a value": implements the logic to allow values with no types to be strings
+     *         (values: 0=not looking, 1=looking, 3=found)
+     *  public:
+     *    isf - used to indicate an xml-rpc response fault (1), invalid xml-rpc fault (2), xml parsing fault (3)
+     *    isf_reason - used for storing xml-rpc response fault string
+     *    value - used to store the value in responses
+     *    method - used to store method name in requests
+     *    params - used to store parameters in requests
+     *    pt - used to store the type of each received parameter. Useful if parameters are automatically decoded to php values
+     *    rt - 'methodcall', 'methodresponse', 'value' or 'fault' (the last one used only in EPI emulation mode)
+     */
+    protected $_xh = array(
+        'ac' => '',
+        'stack' => array(),
+        'valuestack' => array(),
+        'lv' => 0,
+        'isf' => 0,
+        'isf_reason' => '',
+        'value' => null,
+        'method' => false,
+        'params' => array(),
+        'pt' => array(),
+        'rt' => '',
+    );
+
+    /**
+     * @var array[]
+     */
+    protected $xmlrpc_valid_parents = array(
+        'VALUE' => array('MEMBER', 'DATA', 'PARAM', 'FAULT'),
+        'BOOLEAN' => array('VALUE'),
+        'I4' => array('VALUE'),
+        'I8' => array('VALUE'),
+        'EX:I8' => array('VALUE'),
+        'INT' => array('VALUE'),
+        'STRING' => array('VALUE'),
+        'DOUBLE' => array('VALUE'),
+        'DATETIME.ISO8601' => array('VALUE'),
+        'BASE64' => array('VALUE'),
+        'MEMBER' => array('STRUCT'),
+        'NAME' => array('MEMBER'),
+        'DATA' => array('ARRAY'),
+        'ARRAY' => array('VALUE'),
+        'STRUCT' => array('VALUE'),
+        'PARAM' => array('PARAMS'),
+        'METHODNAME' => array('METHODCALL'),
+        'PARAMS' => array('METHODCALL', 'METHODRESPONSE'),
+        'FAULT' => array('METHODRESPONSE'),
+        'NIL' => array('VALUE'), // only used when extension activated
+        'EX:NIL' => array('VALUE'), // only used when extension activated
+    );
+
+    /** @var array $parsing_options */
+    protected $parsing_options = array();
+
+    /** @var int $accept self::ACCEPT_REQUEST | self::ACCEPT_RESPONSE by default */
+    //protected $accept = 3;
+
+    /** @var int $maxChunkLength 4 MB by default. Any value below 10MB should be good */
+    protected $maxChunkLength = 4194304;
+    /** @var array
+     * Used keys: accept, target_charset, methodname_callback, plus the ones set here.
+     * We initialize it partially to help keep BC with subclasses which might have reimplemented `parse()` but not
+     * the element handler methods
+     */
+    protected $current_parsing_options = array(
+        'xmlrpc_null_extension' => false,
+        'xmlrpc_return_datetimes' => false,
+        'xmlrpc_reject_invalid_values' => false
+    );
+
+    /**
+     * @param array $options integer keys: options passed to the inner xml parser
+     *                       string keys:
+     *                       - target_charset (string)
+     *                       - methodname_callback (callable)
+     *                       - xmlrpc_null_extension (bool)
+     *                       - xmlrpc_return_datetimes (bool)
+     *                       - xmlrpc_reject_invalid_values (bool)
+     */
+    public function __construct(array $options = array())
+    {
+        $this->parsing_options = $options;
+    }
+
+    /**
+     * Parses an xml-rpc xml string. Results of the parsing are found in $this->['_xh'].
+     * Logs to the error log any issues which do not cause the parsing to fail.
+     *
+     * @param string $data
+     * @param string $returnType self::RETURN_XMLRPCVALS, self::RETURN_PHP, self::RETURN_EPIVALS
+     * @param int $accept a bit-combination of self::ACCEPT_REQUEST, self::ACCEPT_RESPONSE, self::ACCEPT_VALUE
+     * @param array $options integer-key options are passed to the xml parser, string-key options are used independently.
+     *                       These options are added to options received in the constructor.
+     *                       Note that if options xmlrpc_null_extension, xmlrpc_return_datetimes and xmlrpc_reject_invalid_values
+     *                       are not set, the default settings from PhpXmlRpc\PhpXmlRpc are used
+     * @return array see the definition of $this->_xh for the meaning of the results
+     * @throws \Exception this can happen if a callback function is set and it does throw (i.e. we do not catch exceptions)
+     *
+     * @todo refactor? we could 1. return the parsed data structure, and 2. move $returnType and $accept into options
+     * @todo feature-creep make it possible to pass in options overriding usage of PhpXmlRpc::$xmlrpc_XXX_format, so
+     *       that parsing will be completely independent of global state. Note that it might incur a small perf hit...
+     */
+    public function parse($data, $returnType = self::RETURN_XMLRPCVALS, $accept = 3, $options = array())
+    {
+        $this->_xh = array(
+            'ac' => '',
+            'stack' => array(),
+            'valuestack' => array(),
+            'lv' => 0,
+            'isf' => 0,
+            'isf_reason' => '',
+            'value' => null,
+            'method' => false, // so we can check later if we got a methodname or not
+            'params' => array(),
+            'pt' => array(),
+            'rt' => '',
+        );
+
+        $len = strlen($data);
+
+        // we test for empty documents here to save on resource allocation and simplify the chunked-parsing loop below
+        if ($len == 0) {
+            $this->_xh['isf'] = 3;
+            $this->_xh['isf_reason'] = 'XML error 5: empty document';
+            return $this->_xh;
+        }
+
+        $this->current_parsing_options = array('accept' => $accept);
+
+        $mergedOptions = $this->parsing_options;
+        foreach ($options as $key => $val) {
+            $mergedOptions[$key] = $val;
+        }
+
+        foreach ($mergedOptions as $key => $val) {
+            // q: can php be built without ctype? should we use a regexp?
+            if (is_string($key) && !ctype_digit($key)) {
+                /// @todo on invalid options, throw/error-out instead of logging an error message?
+                switch($key) {
+                    case 'target_charset':
+                        if (function_exists('mb_convert_encoding')) {
+                            $this->current_parsing_options['target_charset'] = $val;
+                        } else {
+                            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ": 'target_charset' option is unsupported without mbstring");
+                        }
+                        break;
+
+                    case 'methodname_callback':
+                        if (is_callable($val)) {
+                            $this->current_parsing_options['methodname_callback'] = $val;
+                        } else {
+                            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ": Callback passed as 'methodname_callback' is not callable");
+                        }
+                        break;
+
+                    case 'xmlrpc_null_extension':
+                    case 'xmlrpc_return_datetimes':
+                    case 'xmlrpc_reject_invalid_values':
+                        $this->current_parsing_options[$key] = $val;
+                        break;
+
+                    default:
+                        $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ": unsupported option: $key");
+                }
+                unset($mergedOptions[$key]);
+            }
+        }
+
+        if (!isset($this->current_parsing_options['xmlrpc_null_extension'])) {
+            $this->current_parsing_options['xmlrpc_null_extension'] = PhpXmlRpc::$xmlrpc_null_extension;
+        }
+        if (!isset($this->current_parsing_options['xmlrpc_return_datetimes'])) {
+            $this->current_parsing_options['xmlrpc_return_datetimes'] = PhpXmlRpc::$xmlrpc_return_datetimes;
+        }
+        if (!isset($this->current_parsing_options['xmlrpc_reject_invalid_values'])) {
+            $this->current_parsing_options['xmlrpc_reject_invalid_values'] = PhpXmlRpc::$xmlrpc_reject_invalid_values;
+        }
+
+        // NB: we use '' instead of null to force charset detection from the xml declaration
+        $parser = xml_parser_create('');
+
+        foreach ($mergedOptions as $key => $val) {
+            xml_parser_set_option($parser, $key, $val);
+        }
+
+        // always set this, in case someone tries to disable it via options...
+        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 1);
+
+        xml_set_object($parser, $this);
+
+        switch ($returnType) {
+            case self::RETURN_PHP:
+                xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast');
+                break;
+            case self::RETURN_EPIVALS:
+                xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_epi');
+                break;
+            /// @todo log an error / throw / error-out on unsupported return type
+            case XMLParser::RETURN_XMLRPCVALS:
+            default:
+                xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee');
+        }
+
+        xml_set_character_data_handler($parser, 'xmlrpc_cd');
+        xml_set_default_handler($parser, 'xmlrpc_dh');
+
+        try {
+            // @see ticket #70 - we have to parse big xml docs in chunks to avoid errors
+            for ($offset = 0; $offset < $len; $offset += $this->maxChunkLength) {
+                $chunk = substr($data, $offset, $this->maxChunkLength);
+                // error handling: xml not well formed
+                if (!xml_parse($parser, $chunk, $offset + $this->maxChunkLength >= $len)) {
+                    $errCode = xml_get_error_code($parser);
+                    $errStr = sprintf('XML error %s: %s at line %d, column %d', $errCode, xml_error_string($errCode),
+                        xml_get_current_line_number($parser), xml_get_current_column_number($parser));
+
+                    $this->_xh['isf'] = 3;
+                    $this->_xh['isf_reason'] = $errStr;
+                }
+                // no need to parse further if we already have a fatal error
+                if ($this->_xh['isf'] >= 2) {
+                    break;
+                }
+            }
+        /// @todo bump minimum php version to 5.5 and use a finally clause instead of doing cleanup 3 times
+        } catch (\Exception $e) {
+            xml_parser_free($parser);
+            $this->current_parsing_options = array();
+            /// @todo should we set $this->_xh['isf'] and $this->_xh['isf_reason'] ?
+            throw $e;
+        } catch (\Error $e) {
+            xml_parser_free($parser);
+            $this->current_parsing_options = array();
+                //$this->accept = $prevAccept;
+                /// @todo should we set $this->_xh['isf'] and $this->_xh['isf_reason'] ?
+            throw $e;
+        }
+
+        xml_parser_free($parser);
+        $this->current_parsing_options = array();
+
+        return $this->_xh;
+    }
+
+    /**
+     * xml parser handler function for opening element tags.
+     * @internal
+     *
+     * @param resource $parser
+     * @param string $name
+     * @param $attrs
+     * @param bool $acceptSingleVals DEPRECATED use the $accept parameter instead
+     * @return void
+     *
+     * @todo optimization creep: throw when setting $this->_xh['isf'] > 1, to completely avoid further xml parsing
+     *       and remove the checking for $this->_xh['isf'] >= 2 everywhere
+     */
+    public function xmlrpc_se($parser, $name, $attrs, $acceptSingleVals = false)
+    {
+        // if invalid xml-rpc already detected, skip all processing
+        if ($this->_xh['isf'] >= 2) {
+            return;
+        }
+
+        // check for correct element nesting
+        if (count($this->_xh['stack']) == 0) {
+            // top level element can only be of 2 types
+            /// @todo optimization creep: save this check into a bool variable, instead of using count() every time:
+            ///       there is only a single top level element in xml anyway
+
+            // BC
+            if ($acceptSingleVals === false) {
+                $accept = $this->current_parsing_options['accept'];
+            } else {
+                $this->logDeprecation('Using argument $acceptSingleVals for method ' . __METHOD__ . ' is deprecated');
+                $accept = self::ACCEPT_REQUEST | self::ACCEPT_RESPONSE | self::ACCEPT_VALUE;
+            }
+            if (($name == 'METHODCALL' && ($accept & self::ACCEPT_REQUEST)) ||
+                ($name == 'METHODRESPONSE' && ($accept & self::ACCEPT_RESPONSE)) ||
+                ($name == 'VALUE' && ($accept & self::ACCEPT_VALUE)) ||
+                ($name == 'FAULT' && ($accept & self::ACCEPT_FAULT))) {
+                $this->_xh['rt'] = strtolower($name);
+            } else {
+                $this->_xh['isf'] = 2;
+                $this->_xh['isf_reason'] = 'missing top level xmlrpc element. Found: ' . $name;
+
+                return;
+            }
+        } else {
+            // not top level element: see if parent is OK
+            $parent = end($this->_xh['stack']);
+            if (!array_key_exists($name, $this->xmlrpc_valid_parents) || !in_array($parent, $this->xmlrpc_valid_parents[$name])) {
+                $this->_xh['isf'] = 2;
+                $this->_xh['isf_reason'] = "xmlrpc element $name cannot be child of $parent";
+
+                return;
+            }
+        }
+
+        switch ($name) {
+            // optimize for speed switch cases: most common cases first
+            case 'VALUE':
+                /// @todo we could check for 2 VALUE elements inside a MEMBER or PARAM element
+                $this->_xh['vt'] = 'value'; // indicator: no value found yet
+                $this->_xh['ac'] = '';
+                $this->_xh['lv'] = 1;
+                $this->_xh['php_class'] = null;
+                break;
+
+            case 'I8':
+            case 'EX:I8':
+                if (PHP_INT_SIZE === 4) {
+                    // INVALID ELEMENT: RAISE ISF so that it is later recognized!!!
+                    $this->_xh['isf'] = 2;
+                    $this->_xh['isf_reason'] = "Received i8 element but php is compiled in 32 bit mode";
+
+                    return;
+                }
+                // fall through voluntarily
+
+            case 'I4':
+            case 'INT':
+            case 'STRING':
+            case 'BOOLEAN':
+            case 'DOUBLE':
+            case 'DATETIME.ISO8601':
+            case 'BASE64':
+                if ($this->_xh['vt'] != 'value') {
+                    // two data elements inside a value: an error occurred!
+                    $this->_xh['isf'] = 2;
+                    $this->_xh['isf_reason'] = "$name element following a {$this->_xh['vt']} element inside a single value";
+
+                    return;
+                }
+                $this->_xh['ac'] = ''; // reset the accumulator
+                break;
+
+            case 'STRUCT':
+            case 'ARRAY':
+                if ($this->_xh['vt'] != 'value') {
+                    // two data elements inside a value: an error occurred!
+                    $this->_xh['isf'] = 2;
+                    $this->_xh['isf_reason'] = "$name element following a {$this->_xh['vt']} element inside a single value";
+
+                    return;
+                }
+                // create an empty array to hold child values, and push it onto appropriate stack
+                $curVal = array(
+                    'values' => array(),
+                    'type' => $name,
+                );
+                // check for out-of-band information to rebuild php objs and, in case it is found, save it
+                if (@isset($attrs['PHP_CLASS'])) {
+                    $curVal['php_class'] = $attrs['PHP_CLASS'];
+                }
+                $this->_xh['valuestack'][] = $curVal;
+                $this->_xh['vt'] = 'data'; // be prepared for a data element next
+                break;
+
+            case 'DATA':
+                if ($this->_xh['vt'] != 'data') {
+                    // two data elements inside a value: an error occurred!
+                    $this->_xh['isf'] = 2;
+                    $this->_xh['isf_reason'] = "found two data elements inside an array element";
+
+                    return;
+                }
+
+            case 'METHODCALL':
+            case 'METHODRESPONSE':
+            case 'PARAMS':
+                // valid elements that add little to processing
+                break;
+
+            case 'METHODNAME':
+            case 'NAME':
+                /// @todo we could check for 2 NAME elements inside a MEMBER element
+                $this->_xh['ac'] = '';
+                break;
+
+            case 'FAULT':
+                $this->_xh['isf'] = 1;
+                break;
+
+            case 'MEMBER':
+                // set member name to null, in case we do not find in the xml later on
+                $this->_xh['valuestack'][count($this->_xh['valuestack']) - 1]['name'] = null;
+                //$this->_xh['ac']='';
+                // Drop trough intentionally
+
+            case 'PARAM':
+                // clear value type, so we can check later if no value has been passed for this param/member
+                $this->_xh['vt'] = null;
+                break;
+
+            case 'NIL':
+            case 'EX:NIL':
+                if ($this->current_parsing_options['xmlrpc_null_extension']) {
+                    if ($this->_xh['vt'] != 'value') {
+                        // two data elements inside a value: an error occurred!
+                        $this->_xh['isf'] = 2;
+                        $this->_xh['isf_reason'] = "$name element following a {$this->_xh['vt']} element inside a single value";
+
+                        return;
+                    }
+                    // reset the accumulator - q: is this necessary at all here? we don't use it on _ee anyway for NILs
+                    $this->_xh['ac'] = '';
+
+                } else {
+                    $this->_xh['isf'] = 2;
+                    $this->_xh['isf_reason'] = 'Invalid NIL value received. Support for NIL can be enabled via \\PhpXmlRpc\\PhpXmlRpc::$xmlrpc_null_extension';
+
+                    return;
+                }
+                break;
+
+            default:
+                // INVALID ELEMENT: RAISE ISF so that it is later recognized
+                /// @todo feature creep = allow a callback instead
+                $this->_xh['isf'] = 2;
+                $this->_xh['isf_reason'] = "found not-xmlrpc xml element $name";
+
+                return;
+        }
+
+        // Save current element name to stack, to validate nesting
+        $this->_xh['stack'][] = $name;
+
+        /// @todo optimization creep: move this inside the big switch() above
+        if ($name != 'VALUE') {
+            $this->_xh['lv'] = 0;
+        }
+    }
+
+    /**
+     * xml parser handler function for close element tags.
+     * @internal
+     *
+     * @param resource $parser
+     * @param string $name
+     * @param int $rebuildXmlrpcvals >1 for rebuilding xmlrpcvals, 0 for rebuilding php values, -1 for xmlrpc-extension compatibility
+     * @return void
+     * @throws \Exception this can happen if a callback function is set and it does throw (i.e. we do not catch exceptions)
+     *
+     * @todo optimization creep: throw when setting $this->_xh['isf'] > 1, to completely avoid further xml parsing
+     *       and remove the checking for $this->_xh['isf'] >= 2 everywhere
+     */
+    public function xmlrpc_ee($parser, $name, $rebuildXmlrpcvals = 1)
+    {
+        if ($this->_xh['isf'] >= 2) {
+            return;
+        }
+
+        // push this element name from stack
+        // NB: if XML validates, correct opening/closing is guaranteed and we do not have to check for $name == $currElem.
+        // we also checked for proper nesting at start of elements...
+        $currElem = array_pop($this->_xh['stack']);
+
+        switch ($name) {
+            case 'VALUE':
+                // If no scalar was inside <VALUE></VALUE>, it was a string value
+                if ($this->_xh['vt'] == 'value') {
+                    $this->_xh['value'] = $this->_xh['ac'];
+                    $this->_xh['vt'] = Value::$xmlrpcString;
+                }
+
+                // in case there is charset conversion required, do it here, to catch both cases of string values
+                if (isset($this->current_parsing_options['target_charset']) && $this->_xh['vt'] === Value::$xmlrpcString) {
+                    $this->_xh['value'] = mb_convert_encoding($this->_xh['value'], $this->current_parsing_options['target_charset'], 'UTF-8');
+                }
+
+                if ($rebuildXmlrpcvals > 0) {
+                    // build the xml-rpc val out of the data received, and substitute it
+                    $temp = new Value($this->_xh['value'], $this->_xh['vt']);
+                    // in case we got info about underlying php class, save it in the object we're rebuilding
+                    if (isset($this->_xh['php_class'])) {
+                        $temp->_php_class = $this->_xh['php_class'];
+                    }
+                    $this->_xh['value'] = $temp;
+                } elseif ($rebuildXmlrpcvals < 0) {
+                    if ($this->_xh['vt'] == Value::$xmlrpcDateTime) {
+                        $this->_xh['value'] = (object)array(
+                            'xmlrpc_type' => 'datetime',
+                            'scalar' => $this->_xh['value'],
+                            'timestamp' => \PhpXmlRpc\Helper\Date::iso8601Decode($this->_xh['value'])
+                        );
+                    } elseif ($this->_xh['vt'] == Value::$xmlrpcBase64) {
+                        $this->_xh['value'] = (object)array(
+                            'xmlrpc_type' => 'base64',
+                            'scalar' => $this->_xh['value']
+                        );
+                    }
+                } else {
+                    /// @todo this should handle php-serialized objects, since std deserializing is done
+                    ///       by php_xmlrpc_decode, which we will not be calling...
+                    //if (isset($this->_xh['php_class'])) {
+                    //}
+                }
+
+                // check if we are inside an array or struct:
+                // if value just built is inside an array, let's move it into array on the stack
+                $vscount = count($this->_xh['valuestack']);
+                if ($vscount && $this->_xh['valuestack'][$vscount - 1]['type'] == 'ARRAY') {
+                    $this->_xh['valuestack'][$vscount - 1]['values'][] = $this->_xh['value'];
+                }
+                break;
+
+            case 'STRING':
+                $this->_xh['vt'] = Value::$xmlrpcString;
+                $this->_xh['lv'] = 3; // indicate we've found a value
+                $this->_xh['value'] = $this->_xh['ac'];
+                break;
+
+            case 'BOOLEAN':
+                $this->_xh['vt'] = Value::$xmlrpcBoolean;
+                $this->_xh['lv'] = 3; // indicate we've found a value
+                // We translate boolean 1 or 0 into PHP constants true or false. Strings 'true' and 'false' are accepted,
+                // even though the spec never mentions them (see e.g. Blogger api docs)
+                // NB: this simple checks helps a lot sanitizing input, i.e. no security problems around here
+                // Note the non-strict type check: it will allow ' 1 '
+                /// @todo feature-creep: use a flexible regexp, the same as we do with int, double and datetime.
+                ///       Note that using a regexp would also make this test less sensitive to phpunit shenanigans, and
+                ///       to changes in the way php compares strings (since 8.0, leading and trailing newlines are
+                ///       accepted when deciding if a string numeric...)
+                if ($this->_xh['ac'] == '1' || strcasecmp($this->_xh['ac'], 'true') === 0) {
+                    $this->_xh['value'] = true;
+                } else {
+                    // log if receiving something strange, even though we set the value to false anyway
+                    /// @todo to be consistent with the other types, we should return a value outside the good-value domain, e.g. NULL
+                    if ($this->_xh['ac'] != '0' && strcasecmp($this->_xh['ac'], 'false') !== 0) {
+                        if (!$this->handleParsingError('invalid data received in BOOLEAN value: ' .
+                            $this->truncateValueForLog($this->_xh['ac']), __METHOD__)) {
+                            return;
+                        }
+                    }
+                    $this->_xh['value'] = false;
+                }
+                break;
+
+            case 'EX:I8':
+                $name = 'i8';
+                // fall through voluntarily
+            case 'I4':
+            case 'I8':
+            case 'INT':
+                // NB: we build the Value object with the original xml element name found, except for ex:i8. The
+                // `Value::scalarTyp()` function will do some normalization of the data
+                $this->_xh['vt'] = strtolower($name);
+                $this->_xh['lv'] = 3; // indicate we've found a value
+                if (!preg_match(PhpXmlRpc::$xmlrpc_int_format, $this->_xh['ac'])) {
+                    if (!$this->handleParsingError('non numeric data received in INT value: ' .
+                        $this->truncateValueForLog($this->_xh['ac']), __METHOD__)) {
+                        return;
+                    }
+                    /// @todo: find a better way of reporting an error value than this! Use NaN?
+                    $this->_xh['value'] = 'ERROR_NON_NUMERIC_FOUND';
+                } else {
+                    // it's ok, add it on
+                    $this->_xh['value'] = (int)$this->_xh['ac'];
+                }
+                break;
+
+            case 'DOUBLE':
+                $this->_xh['vt'] = Value::$xmlrpcDouble;
+                $this->_xh['lv'] = 3; // indicate we've found a value
+                if (!preg_match(PhpXmlRpc::$xmlrpc_double_format, $this->_xh['ac'])) {
+                    if (!$this->handleParsingError('non numeric data received in DOUBLE value: ' .
+                        $this->truncateValueForLog($this->_xh['ac']), __METHOD__)) {
+                        return;
+                    }
+
+                    $this->_xh['value'] = 'ERROR_NON_NUMERIC_FOUND';
+                } else {
+                    // it's ok, add it on
+                    $this->_xh['value'] = (double)$this->_xh['ac'];
+                }
+                break;
+
+            case 'DATETIME.ISO8601':
+                $this->_xh['vt'] = Value::$xmlrpcDateTime;
+                $this->_xh['lv'] = 3; // indicate we've found a value
+                if (!preg_match(PhpXmlRpc::$xmlrpc_datetime_format, $this->_xh['ac'])) {
+                    if (!$this->handleParsingError('invalid data received in DATETIME value: ' .
+                        $this->truncateValueForLog($this->_xh['ac']), __METHOD__)) {
+                        return;
+                    }
+                }
+                if ($this->current_parsing_options['xmlrpc_return_datetimes']) {
+                    try {
+                        $this->_xh['value'] = new \DateTime($this->_xh['ac']);
+
+                    // the default regex used to validate the date string a few lines above should make this case impossible,
+                    // but one never knows...
+                    } catch(\Exception $e) {
+                        // what to do? We can not guarantee that a valid date can be created. We return null...
+                        if (!$this->handleParsingError('invalid data received in DATETIME value. Error ' .
+                            $e->getMessage(), __METHOD__)) {
+                            return;
+                        }
+                    }
+                } else {
+                    $this->_xh['value'] = $this->_xh['ac'];
+                }
+                break;
+
+            case 'BASE64':
+                $this->_xh['vt'] = Value::$xmlrpcBase64;
+                $this->_xh['lv'] = 3; // indicate we've found a value
+                if ($this->current_parsing_options['xmlrpc_reject_invalid_values']) {
+                    $v = base64_decode($this->_xh['ac'], true);
+                    if ($v === false) {
+                        $this->_xh['isf'] = 2;
+                        $this->_xh['isf_reason'] = 'Invalid data received in BASE64 value: '. $this->truncateValueForLog($this->_xh['ac']);
+                        return;
+                    }
+                } else {
+                    $v = base64_decode($this->_xh['ac']);
+                    if ($v === '' && $this->_xh['ac'] !== '') {
+                        // only the empty string should decode to the empty string
+                        $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': invalid data received in BASE64 value: ' .
+                            $this->truncateValueForLog($this->_xh['ac']));
+                    }
+                }
+                $this->_xh['value'] = $v;
+                break;
+
+            case 'NAME':
+                $this->_xh['valuestack'][count($this->_xh['valuestack']) - 1]['name'] = $this->_xh['ac'];
+                break;
+
+            case 'MEMBER':
+                // add to array in the stack the last element built, unless no VALUE or no NAME were found
+                if ($this->_xh['vt']) {
+                    $vscount = count($this->_xh['valuestack']);
+                    if ($this->_xh['valuestack'][$vscount - 1]['name'] === null) {
+                        if (!$this->handleParsingError('missing NAME inside STRUCT in received xml', __METHOD__)) {
+                            return;
+                        }
+                        $this->_xh['valuestack'][$vscount - 1]['name'] = '';
+                    }
+                    $this->_xh['valuestack'][$vscount - 1]['values'][$this->_xh['valuestack'][$vscount - 1]['name']] = $this->_xh['value'];
+                } else {
+                    if (!$this->handleParsingError('missing VALUE inside STRUCT in received xml', __METHOD__)) {
+                        return;
+                    }
+                }
+                break;
+
+            case 'DATA':
+                $this->_xh['vt'] = null; // reset this to check for 2 data elements in a row - even if they're empty
+                break;
+
+            case 'STRUCT':
+            case 'ARRAY':
+                // fetch out of stack array of values, and promote it to current value
+                $currVal = array_pop($this->_xh['valuestack']);
+                $this->_xh['value'] = $currVal['values'];
+                $this->_xh['vt'] = strtolower($name);
+                if (isset($currVal['php_class'])) {
+                    $this->_xh['php_class'] = $currVal['php_class'];
+                }
+                break;
+
+            case 'PARAM':
+                // add to array of params the current value, unless no VALUE was found
+                /// @todo should we also check if there were two VALUE inside the PARAM?
+                if ($this->_xh['vt']) {
+                    $this->_xh['params'][] = $this->_xh['value'];
+                    $this->_xh['pt'][] = $this->_xh['vt'];
+                } else {
+                    if (!$this->handleParsingError('missing VALUE inside PARAM in received xml', __METHOD__)) {
+                        return;
+                    }
+                }
+                break;
+
+            case 'METHODNAME':
+                if (!preg_match(PhpXmlRpc::$xmlrpc_methodname_format, $this->_xh['ac'])) {
+                    if (!$this->handleParsingError('invalid data received in METHODNAME: '.
+                        $this->truncateValueForLog($this->_xh['ac']), __METHOD__)) {
+                        return;
+                    }
+                }
+                $methodName = trim($this->_xh['ac']);
+                $this->_xh['method'] = $methodName;
+                // we allow the callback to f.e. give us back a mangled method name by manipulating $this
+                if (isset($this->current_parsing_options['methodname_callback'])) {
+                    call_user_func($this->current_parsing_options['methodname_callback'], $methodName, $this, $parser);
+                }
+                break;
+
+            case 'NIL':
+            case 'EX:NIL':
+                // NB: if NIL support is not enabled, parsing stops at element start. So this If is redundant
+                //if ($this->current_parsing_options['xmlrpc_null_extension']) {
+                    $this->_xh['vt'] = 'null';
+                    $this->_xh['value'] = null;
+                    $this->_xh['lv'] = 3;
+                //}
+                break;
+
+            /// @todo add extra checking:
+            ///       - METHODRESPONSE should contain either a PARAMS with a single PARAM, or a FAULT
+            ///       - FAULT should contain a single struct with the 2 expected members (check their name and type)
+            ///       - METHODCALL should contain a methodname
+            case 'PARAMS':
+            case 'FAULT':
+            case 'METHODCALL':
+            case 'METHODRESPONSE':
+                break;
+
+            default:
+                // End of INVALID ELEMENT
+                // Should we add an assert here for unreachable code? When an invalid element is found in xmlrpc_se,
+                // $this->_xh['isf'] is set to 2...
+                break;
+        }
+    }
+
+    /**
+     * Used in decoding xml-rpc requests/responses without rebuilding xml-rpc Values.
+     * @internal
+     *
+     * @param resource $parser
+     * @param string $name
+     * @return void
+     */
+    public function xmlrpc_ee_fast($parser, $name)
+    {
+        $this->xmlrpc_ee($parser, $name, 0);
+    }
+
+    /**
+     * Used in decoding xml-rpc requests/responses while building xmlrpc-extension Values (plain php for all but base64 and datetime).
+     * @internal
+     *
+     * @param resource $parser
+     * @param string $name
+     * @return void
+     */
+    public function xmlrpc_ee_epi($parser, $name)
+    {
+        $this->xmlrpc_ee($parser, $name, -1);
+    }
+
+    /**
+     * xml parser handler function for character data.
+     * @internal
+     *
+     * @param resource $parser
+     * @param string $data
+     * @return void
+     */
+    public function xmlrpc_cd($parser, $data)
+    {
+        // skip processing if xml fault already detected
+        if ($this->_xh['isf'] >= 2) {
+            return;
+        }
+
+        // "lookforvalue == 3" means that we've found an entire value and should discard any further character data
+        if ($this->_xh['lv'] != 3) {
+            $this->_xh['ac'] .= $data;
+        }
+    }
+
+    /**
+     * xml parser handler function for 'other stuff', i.e. not char data or element start/end tag.
+     * In fact, it only gets called on unknown entities...
+     * @internal
+     *
+     * @param $parser
+     * @param string data
+     * @return void
+     */
+    public function xmlrpc_dh($parser, $data)
+    {
+        // skip processing if xml fault already detected
+        if ($this->_xh['isf'] >= 2) {
+            return;
+        }
+
+        if (substr($data, 0, 1) == '&' && substr($data, -1, 1) == ';') {
+            $this->_xh['ac'] .= $data;
+        }
+    }
+
+    /**
+     * xml charset encoding guessing helper function.
+     * Tries to determine the charset encoding of an XML chunk received over HTTP.
+     *
+     * NB: according to the spec (RFC 3023), if text/xml content-type is received over HTTP without a content-type,
+     * we SHOULD assume it is strictly US-ASCII. But we try to be more tolerant of non-conforming (legacy?) clients/servers,
+     * which will be most probably using UTF-8 anyway...
+     * In order of importance checks:
+     * 1. http headers
+     * 2. BOM
+     * 3. XML declaration
+     * 4. guesses using mb_detect_encoding()
+     *
+     * @param string $httpHeader the http Content-type header
+     * @param string $xmlChunk xml content buffer
+     * @param string $encodingPrefs comma separated list of character encodings to be used as default (when mb extension is enabled).
+     *                              This can also be set globally using PhpXmlRpc::$xmlrpc_detectencodings
+     * @return string the encoding determined. Null if it can't be determined and mbstring is enabled,
+     *                PhpXmlRpc::$xmlrpc_defencoding if it can't be determined and mbstring is not enabled
+     *
+     * @todo as of 2023, the relevant RFC for XML Media Types is now 7303, and for HTTP it is 9110. Check if the order of
+     *       precedence implemented here is still correct
+     * @todo explore usage of mb_http_input(): does it detect http headers + post data? if so, use it instead of hand-detection!!!
+     * @todo feature-creep make it possible to pass in options overriding usage of PhpXmlRpc static variables, to make
+     *       the method independent of global state
+     */
+    public static function guessEncoding($httpHeader = '', $xmlChunk = '', $encodingPrefs = null)
+    {
+        // discussion: see http://www.yale.edu/pclt/encoding/
+        // 1 - test if encoding is specified in HTTP HEADERS
+
+        // Details:
+        // LWS:           (\13\10)?( |\t)+
+        // token:         (any char but excluded stuff)+
+        // quoted string: " (any char but double quotes and control chars)* "
+        // header:        Content-type = ...; charset=value(; ...)*
+        //   where value is of type token, no LWS allowed between 'charset' and value
+        // Note: we do not check for invalid chars in VALUE:
+        //   this had better be done using pure ereg as below
+        // Note 2: we might be removing whitespace/tabs that ought to be left in if
+        //   the received charset is a quoted string. But nobody uses such charset names...
+
+        /// @todo this test will pass if ANY header has charset specification, not only Content-Type. Fix it?
+        $matches = array();
+        if (preg_match('/;\s*charset\s*=([^;]+)/i', $httpHeader, $matches)) {
+            return strtoupper(trim($matches[1], " \t\""));
+        }
+
+        // 2 - scan the first bytes of the data for a UTF-16 (or other) BOM pattern
+        //     (source: http://www.w3.org/TR/2000/REC-xml-20001006)
+        //     NOTE: actually, according to the spec, even if we find the BOM and determine
+        //     an encoding, we should check if there is an encoding specified
+        //     in the xml declaration, and verify if they match.
+        /// @todo implement check as described above?
+        /// @todo implement check for first bytes of string even without a BOM? (It sure looks harder than for cases WITH a BOM)
+        if (preg_match('/^(?:\x00\x00\xFE\xFF|\xFF\xFE\x00\x00|\x00\x00\xFF\xFE|\xFE\xFF\x00\x00)/', $xmlChunk)) {
+            return 'UCS-4';
+        } elseif (preg_match('/^(?:\xFE\xFF|\xFF\xFE)/', $xmlChunk)) {
+            return 'UTF-16';
+        } elseif (preg_match('/^(?:\xEF\xBB\xBF)/', $xmlChunk)) {
+            return 'UTF-8';
+        }
+
+        // 3 - test if encoding is specified in the xml declaration
+        /// @todo this regexp will fail if $xmlChunk uses UTF-32/UCS-4, and most likely UTF-16/UCS-2 as well. In that
+        ///       case we leave the guesswork up to mbstring - which seems to be able to detect it, starting with php 5.6.
+        ///       For lower versions, we could attempt usage of mb_ereg...
+        // Details:
+        // SPACE:         (#x20 | #x9 | #xD | #xA)+ === [ \x9\xD\xA]+
+        // EQ:            SPACE?=SPACE? === [ \x9\xD\xA]*=[ \x9\xD\xA]*
+        if (preg_match('/^<\?xml\s+version\s*=\s*' . "((?:\"[a-zA-Z0-9_.:-]+\")|(?:'[a-zA-Z0-9_.:-]+'))" .
+            '\s+encoding\s*=\s*' . "((?:\"[A-Za-z][A-Za-z0-9._-]*\")|(?:'[A-Za-z][A-Za-z0-9._-]*'))/",
+            $xmlChunk, $matches)) {
+            return strtoupper(substr($matches[2], 1, -1));
+        }
+
+        // 4 - if mbstring is available, let it do the guesswork
+        if (function_exists('mb_detect_encoding')) {
+            if ($encodingPrefs == null && PhpXmlRpc::$xmlrpc_detectencodings != null) {
+                $encodingPrefs = PhpXmlRpc::$xmlrpc_detectencodings;
+            }
+            if ($encodingPrefs) {
+                $enc = mb_detect_encoding($xmlChunk, $encodingPrefs);
+            } else {
+                $enc = mb_detect_encoding($xmlChunk);
+            }
+            // NB: mb_detect likes to call it ascii, xml parser likes to call it US_ASCII...
+            // IANA also likes better US-ASCII, so go with it
+            if ($enc == 'ASCII') {
+                $enc = 'US-' . $enc;
+            }
+
+            return $enc;
+        } else {
+            // No encoding specified: assume it is iso-8859-1, as per HTTP1.1?
+            // Both RFC 2616 (HTTP 1.1) and RFC 1945 (HTTP 1.0) clearly state that for text/xxx content types
+            // this should be the standard. And we should be getting text/xml as request and response.
+            // BUT we have to be backward compatible with the lib, which always used UTF-8 as default. Moreover,
+            // RFC 7231, which obsoletes the two RFC mentioned above, has changed the rules. It says:
+            // "The default charset of ISO-8859-1 for text media types has been removed; the default is now whatever
+            // the media type definition says."
+            return PhpXmlRpc::$xmlrpc_defencoding;
+        }
+    }
+
+    /**
+     * Helper function: checks if an xml chunk has a charset declaration (BOM or in the xml declaration).
+     *
+     * @param string $xmlChunk
+     * @return bool
+     *
+     * @todo rename to hasEncodingDeclaration
+     */
+    public static function hasEncoding($xmlChunk)
+    {
+        // scan the first bytes of the data for a UTF-16 (or other) BOM pattern
+        //     (source: http://www.w3.org/TR/2000/REC-xml-20001006)
+        if (preg_match('/^(?:\x00\x00\xFE\xFF|\xFF\xFE\x00\x00|\x00\x00\xFF\xFE|\xFE\xFF\x00\x00)/', $xmlChunk)) {
+            return true;
+        } elseif (preg_match('/^(?:\xFE\xFF|\xFF\xFE)/', $xmlChunk)) {
+            return true;
+        } elseif (preg_match('/^(?:\xEF\xBB\xBF)/', $xmlChunk)) {
+            return true;
+        }
+
+        // test if encoding is specified in the xml declaration
+        // Details:
+        // SPACE:         (#x20 | #x9 | #xD | #xA)+ === [ \x9\xD\xA]+
+        // EQ:            SPACE?=SPACE? === [ \x9\xD\xA]*=[ \x9\xD\xA]*
+        if (preg_match('/^<\?xml\s+version\s*=\s*' . "((?:\"[a-zA-Z0-9_.:-]+\")|(?:'[a-zA-Z0-9_.:-]+'))" .
+            '\s+encoding\s*=\s*' . "((?:\"[A-Za-z][A-Za-z0-9._-]*\")|(?:'[A-Za-z][A-Za-z0-9._-]*'))/",
+            $xmlChunk)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * @param string $message
+     * @param string $method method/file/line info
+     * @return bool false if the caller has to stop parsing
+     */
+    protected function handleParsingError($message, $method = '')
+    {
+        if ($this->current_parsing_options['xmlrpc_reject_invalid_values']) {
+            $this->_xh['isf'] = 2;
+            $this->_xh['isf_reason'] = ucfirst($message);
+            return false;
+        } else {
+            $this->getLogger()->error('XML-RPC: ' . ($method != '' ? $method . ': ' : '') . $message);
+            return true;
+        }
+    }
+
+    /**
+     * Truncates unsafe data
+     * @param string $data
+     * @return string
+     */
+    protected function truncateValueForLog($data)
+    {
+        if (strlen($data) > $this->maxLogValueLength) {
+            return substr($data, 0, $this->maxLogValueLength - 3) . '...';
+        }
+
+        return $data;
+    }
+
+    // *** BC layer ***
+
+    /**
+     * xml parser handler function for opening element tags.
+     * Used in decoding xml chunks that might represent single xml-rpc values as well as requests, responses.
+     * @deprecated
+     *
+     * @param resource $parser
+     * @param $name
+     * @param $attrs
+     * @return void
+     */
+    public function xmlrpc_se_any($parser, $name, $attrs)
+    {
+        // this will be spamming the log if this method is in use...
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        $this->xmlrpc_se($parser, $name, $attrs, true);
+    }
+
+    public function &__get($name)
+    {
+        switch ($name) {
+            case '_xh':
+            case 'xmlrpc_valid_parents':
+                $this->logDeprecation('Getting property XMLParser::' . $name . ' is deprecated');
+                return $this->$name;
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+                $result = null;
+                return $result;
+        }
+    }
+
+    public function __set($name, $value)
+    {
+        switch ($name) {
+            // this should only ever be called by subclasses which overtook `parse()`
+            case 'accept':
+                $this->logDeprecation('Setting property XMLParser::' . $name . ' is deprecated');
+                $this->current_parsing_options['accept'] = $value;
+                break;
+            case '_xh':
+            case 'xmlrpc_valid_parents':
+                $this->logDeprecation('Setting property XMLParser::' . $name . ' is deprecated');
+                $this->$name = $value;
+                break;
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __set(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+        }
+    }
+
+    public function __isset($name)
+    {
+        switch ($name) {
+            case 'accept':
+                $this->logDeprecation('Checking property XMLParser::' . $name . ' is deprecated');
+                return isset($this->current_parsing_options['accept']);
+            case '_xh':
+            case 'xmlrpc_valid_parents':
+                $this->logDeprecation('Checking property XMLParser::' . $name . ' is deprecated');
+                return isset($this->$name);
+            default:
+                return false;
+        }
+    }
+
+    public function __unset($name)
+    {
+        switch ($name) {
+            // q: does this make sense at all?
+            case 'accept':
+                $this->logDeprecation('Unsetting property XMLParser::' . $name . ' is deprecated');
+                unset($this->current_parsing_options['accept']);
+                break;
+            case '_xh':
+            case 'xmlrpc_valid_parents':
+                $this->logDeprecation('Unsetting property XMLParser::' . $name . ' is deprecated');
+                unset($this->$name);
+                break;
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __unset(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+        }
+    }
+}

+ 318 - 0
includes/phpxmlrpc/PhpXmlRpc.php

@@ -0,0 +1,318 @@
+<?php
+
+namespace PhpXmlRpc;
+
+use PhpXmlRpc\Helper\Charset;
+use PhpXmlRpc\Helper\Http;
+use PhpXmlRpc\Helper\Interop;
+use PhpXmlRpc\Helper\XMLParser;
+
+/**
+ * Manages global configuration for operation of the library.
+ */
+class PhpXmlRpc
+{
+    /**
+     * @var int[]
+     */
+    public static $xmlrpcerr = array(
+        'unknown_method' => 1, // server
+        /// @deprecated. left in for BC
+        'invalid_return' => 2, // client
+        'incorrect_params' => 3, // server
+        'introspect_unknown' => 4, // server
+        'http_error' => 5, // client
+        'no_data' => 6, // client
+        'no_ssl' => 7, // client
+        'curl_fail' => 8, // client
+        'invalid_request' => 15, // server
+        'no_curl' => 16, // client
+        'server_error' => 17, // server
+        'multicall_error' => 18, // client
+        'multicall_notstruct' => 9, // client
+        'multicall_nomethod' => 10, // client
+        'multicall_notstring' => 11, // client
+        'multicall_recursion' => 12, // client
+        'multicall_noparams' => 13, // client
+        'multicall_notarray' => 14, // client
+        'no_http2' => 19, // client
+        'unsupported_option' => 20, // client
+        // the following 3 are meant to give greater insight than 'invalid_return'. They use the same code for BC,
+        // but you can override their value in your own code
+        'invalid_xml' => 2, // client
+        'xml_not_compliant' => 2, // client
+        'xml_parsing_error' => 2, // client
+
+        /// @todo verify: can these conflict with $xmlrpcerrxml?
+        'cannot_decompress' => 103,
+        'decompress_fail' => 104,
+        'dechunk_fail' => 105,
+        'server_cannot_decompress' => 106,
+        'server_decompress_fail' => 107,
+    );
+
+    /**
+     * @var string[]
+     */
+    public static $xmlrpcstr = array(
+        'unknown_method' => 'Unknown method',
+        /// @deprecated. left in for BC
+        'invalid_return' => 'Invalid response payload (you can use the setDebug method to allow analysis of the response)',
+        'incorrect_params' => 'Incorrect parameters passed to method',
+        'introspect_unknown' => "Can't introspect: method unknown",
+        'http_error' => "Didn't receive 200 OK from remote server",
+        'no_data' => 'No data received from server',
+        'no_ssl' => 'No SSL support compiled in',
+        'curl_fail' => 'CURL error',
+        'invalid_request' => 'Invalid request payload',
+        'no_curl' => 'No CURL support compiled in',
+        'server_error' => 'Internal server error',
+        'multicall_error' => 'Received from server invalid multicall response',
+        'multicall_notstruct' => 'system.multicall expected struct',
+        'multicall_nomethod' => 'Missing methodName',
+        'multicall_notstring' => 'methodName is not a string',
+        'multicall_recursion' => 'Recursive system.multicall forbidden',
+        'multicall_noparams' => 'Missing params',
+        'multicall_notarray' => 'params is not an array',
+        'no_http2' => 'No HTTP/2 support compiled in',
+        'unsupported_option' => 'Some client option is not supported with the transport method currently in use',
+        // the following 3 are meant to give greater insight than 'invalid_return'. They use the same string for BC,
+        // but you can override their value in your own code
+        'invalid_xml' => 'Invalid response payload (you can use the setDebug method to allow analysis of the response)',
+        'xml_not_compliant' => 'Invalid response payload (you can use the setDebug method to allow analysis of the response)',
+        'xml_parsing_error' => 'Invalid response payload (you can use the setDebug method to allow analysis of the response)',
+
+        'cannot_decompress' => 'Received from server compressed HTTP and cannot decompress',
+        'decompress_fail' => 'Received from server invalid compressed HTTP',
+        'dechunk_fail' => 'Received from server invalid chunked HTTP',
+        'server_cannot_decompress' => 'Received from client compressed HTTP request and cannot decompress',
+        'server_decompress_fail' => 'Received from client invalid compressed HTTP request',
+    );
+
+    /**
+     * @var string
+     * The charset encoding used by the server for received requests and by the client for received responses when
+     * received charset cannot be determined and mbstring extension is not enabled.
+     */
+    public static $xmlrpc_defencoding = "UTF-8";
+    /**
+     * @var string[]
+     * The list of preferred encodings used by the server for requests and by the client for responses to detect the
+     * charset of the received payload when
+     * - the charset cannot be determined by looking at http headers, xml declaration or BOM
+     * - mbstring extension is enabled
+     */
+    public static $xmlrpc_detectencodings = array();
+    /**
+     * @var string
+     * The encoding used internally by PHP.
+     * String values received as xml will be converted to this, and php strings will be converted to xml as if
+     * having been coded with this.
+     * Valid also when defining names of xml-rpc methods
+     */
+    public static $xmlrpc_internalencoding = "UTF-8";
+
+    /**
+     * @var string
+     */
+    public static $xmlrpcName = "XML-RPC for PHP";
+    /**
+     * @var string
+     */
+    public static $xmlrpcVersion = "4.10.1";
+
+    /**
+     * @var int
+     * Let user errors start at 800
+     */
+    public static $xmlrpcerruser = 800;
+    /**
+     * @var int
+     * Let XML parse errors start at 100
+     */
+    public static $xmlrpcerrxml = 100;
+
+    /**
+     * @var bool
+     * Set to TRUE to enable correct decoding of <NIL/> and <EX:NIL/> values
+     */
+    public static $xmlrpc_null_extension = false;
+
+    /**
+     * @var bool
+     * Set to TRUE to make the library use DateTime objects instead of strings for all values parsed from incoming XML.
+     * NB: if the received strings are not parseable as dates, NULL will be returned. To prevent that, enable as
+     * well `xmlrpc_reject_invalid_values`, so that invalid dates will be rejected by the library
+     */
+    public static $xmlrpc_return_datetimes = false;
+
+    /**
+     * @var bool
+     * Set to TRUE to make the library reject incoming xml which uses invalid data for xml-rpc elements, such
+     * as base64 strings which can not be decoded, dateTime strings which do not represent a valid date, invalid bools,
+     * floats and integers, method names with forbidden characters, or struct members missing the value or name
+     */
+    public static $xmlrpc_reject_invalid_values = false;
+
+    /**
+     * @var bool
+     * Set to TRUE to enable encoding of php NULL values to <EX:NIL/> instead of <NIL/>
+     */
+    public static $xmlrpc_null_apache_encoding = false;
+
+    public static $xmlrpc_null_apache_encoding_ns = "http://ws.apache.org/xmlrpc/namespaces/extensions";
+
+    /**
+     * @var int
+     * Number of decimal digits used to serialize Double values.
+     * @todo rename :'-(
+     */
+    public static $xmlpc_double_precision = 128;
+
+    /**
+     * @var string
+     * Used to validate received date values. Alter this if the server/client you are communicating with uses date
+     * formats non-conformant with the spec
+     * NB: the string should not match any data which php can not successfully use in a DateTime object constructor call
+     * NB: atm, the Date helper uses this regexp and expects to find matches in a specific order
+     */
+    public static $xmlrpc_datetime_format = '/^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-4]):([0-5][0-9]):([0-5][0-9]|60)$/';
+
+    /**
+     * @var string
+     * Used to validate received integer values. Alter this if the server/client you are communicating with uses
+     * formats non-conformant with the spec.
+     * We keep in spaces for BC, even though they are forbidden by the spec.
+     * NB: the string should not match any data which php can not successfully cast to an integer
+     */
+    public static $xmlrpc_int_format = '/^[ \t]*[+-]?[0-9]+[ \t]*$/';
+
+    /**
+     * @var string
+     * Used to validate received double values. Alter this if the server/client you are communicating with uses
+     * formats non-conformant with the spec, e.g. with leading/trailing spaces/tabs/newlines.
+     * We keep in spaces for BC, even though they are forbidden by the spec.
+     * NB: the string should not match any data which php can not successfully cast to a float
+     */
+    public static $xmlrpc_double_format = '/^[ \t]*[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?[ \t]*$/';
+
+    /**
+     * @var string
+     * Used to validate received methodname values.
+     * According to the spec: "The string may only contain identifier characters, upper and lower-case A-Z, the numeric
+     * characters, 0-9, underscore, dot, colon and slash".
+     * We keep in leading and trailing spaces for BC, even though they are forbidden by the spec.
+     * But what about "identifier characters"? Is that meant to be 'identifier characters: upper and lower-case A-Z, ...'
+     * or something else? If the latter, there is no consensus across programming languages about what is a valid
+     * identifier character. PHP has one of the most crazy definitions of what is a valid identifier character, allowing
+     * _bytes_ in range x80-xff, without even specifying a character set (and then lowercasing anyway in some cases)...
+     */
+    public static $xmlrpc_methodname_format = '|^[ \t]*[a-zA-Z0-9_.:/]+[ \t]*$|';
+
+    /**
+     * @var bool
+     * Set this to false to have a warning added to the log whenever user code uses a deprecated method/parameter/property
+     */
+    public static $xmlrpc_silence_deprecations = true;
+
+    // *** BC layer ***
+
+    /**
+     * Inject a logger into all classes of the PhpXmlRpc library which use one
+     *
+     * @param $logger
+     * @return void
+     */
+    public static function setLogger($logger)
+    {
+        Charset::setLogger($logger);
+        Client::setLogger($logger);
+        Encoder::setLogger($logger);
+        Http::setLogger($logger);
+        Request::setLogger($logger);
+        Server::setLogger($logger);
+        Value::setLogger($logger);
+        Wrapper::setLogger($logger);
+        XMLParser::setLogger($logger);
+    }
+
+    /**
+     * Makes the library use the error codes detailed at https://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
+     *
+     * @return void
+     *
+     * @tofo feature creep - allow switching back to the original set of codes; querying the current mode
+     */
+    public static function useInteropFaults()
+    {
+        self::$xmlrpcerr = Interop::$xmlrpcerr;
+
+        self::$xmlrpcerruser = -Interop::$xmlrpcerruser;
+    }
+
+    /**
+     * A function to be used for compatibility with legacy code: it creates all global variables which used to be declared,
+     * such as library version etc...
+     * @return void
+     *
+     * @deprecated
+     */
+    public static function exportGlobals()
+    {
+        $reflection = new \ReflectionClass('PhpXmlRpc\PhpXmlRpc');
+        foreach ($reflection->getStaticProperties() as $name => $value) {
+            if (!in_array($name, array('xmlrpc_return_datetimes', 'xmlrpc_reject_invalid_values', 'xmlrpc_datetime_format',
+                'xmlrpc_int_format', 'xmlrpc_double_format', 'xmlrpc_methodname_format', 'xmlrpc_silence_deprecations'))) {
+                $GLOBALS[$name] = $value;
+            }
+        }
+
+        // NB: all the variables exported into the global namespace below here do NOT guarantee 100% compatibility,
+        // as they are NOT reimported back during calls to importGlobals()
+
+        $reflection = new \ReflectionClass('PhpXmlRpc\Value');
+        foreach ($reflection->getStaticProperties() as $name => $value) {
+            if (!in_array($name, array('logger', 'charsetEncoder'))) {
+                $GLOBALS[$name] = $value;
+            }
+        }
+
+        /// @todo mke it possible to inject the XMLParser and Charset, as we do in other classes
+
+        $parser = new Helper\XMLParser();
+        $GLOBALS['xmlrpc_valid_parents'] = $parser->xmlrpc_valid_parents;
+
+        $charset = Charset::instance();
+        $GLOBALS['xml_iso88591_Entities'] = $charset->getEntities('iso88591');
+    }
+
+    /**
+     * A function to be used for compatibility with legacy code: it gets the values of all global variables which used
+     * to be declared, such as library version etc... and sets them to php classes.
+     * It should be used by code which changed the values of those global variables to alter the working of the library.
+     * Example code:
+     * 1. include xmlrpc.inc
+     * 2. set the values, e.g. $GLOBALS['xmlrpc_internalencoding'] = 'UTF-8';
+     * 3. import them: PhpXmlRpc\PhpXmlRpc::importGlobals();
+     * 4. run your own code.
+     *
+     * @return void
+     *
+     * @deprecated
+     *
+     * @todo this function does not import back xmlrpc_valid_parents and xml_iso88591_Entities
+     */
+    public static function importGlobals()
+    {
+        $reflection = new \ReflectionClass('PhpXmlRpc\PhpXmlRpc');
+        foreach ($reflection->getStaticProperties() as $name => $value) {
+            if (!in_array($name, array('xmlrpc_return_datetimes', 'xmlrpc_reject_invalid_values', 'xmlrpc_datetime_format',
+                'xmlrpc_int_format', 'xmlrpc_double_format', 'xmlrpc_methodname_format', 'xmlrpc_silence_deprecations')))
+            {
+                if (isset($GLOBALS[$name])) {
+                    self::$$name = $GLOBALS[$name];
+                }
+            }
+        }
+    }
+}

+ 563 - 0
includes/phpxmlrpc/Request.php

@@ -0,0 +1,563 @@
+<?php
+
+namespace PhpXmlRpc;
+
+use PhpXmlRpc\Exception\HttpException;
+use PhpXmlRpc\Helper\Http;
+use PhpXmlRpc\Helper\XMLParser;
+use PhpXmlRpc\Traits\CharsetEncoderAware;
+use PhpXmlRpc\Traits\DeprecationLogger;
+use PhpXmlRpc\Traits\ParserAware;
+use PhpXmlRpc\Traits\PayloadBearer;
+
+/**
+ * This class provides the representation of a request to an XML-RPC server.
+ * A client sends a PhpXmlrpc\Request to a server, and receives back an PhpXmlrpc\Response.
+ *
+ * @todo feature creep - add a protected $httpRequest member, in the same way the Response has one
+ *
+ * @property string $methodname deprecated - public access left in purely for BC. Access via method()/__construct()
+ * @property Value[] $params deprecated - public access left in purely for BC. Access via getParam()/__construct()
+ * @property int $debug deprecated - public access left in purely for BC. Access via .../setDebug()
+ * @property string $payload deprecated - public access left in purely for BC. Access via getPayload()/setPayload()
+ * @property string $content_type deprecated - public access left in purely for BC. Access via getContentType()/setPayload()
+ */
+class Request
+{
+    use CharsetEncoderAware;
+    use DeprecationLogger;
+    use ParserAware;
+    use PayloadBearer;
+
+    /** @var string */
+    protected $methodname;
+    /** @var Value[] */
+    protected $params = array();
+    /** @var int */
+    protected $debug = 0;
+
+    /**
+     * holds data while parsing the response. NB: Not a full Response object
+     * @deprecated will be removed in a future release; still accessible by subclasses for the moment
+     */
+    private $httpResponse = array();
+
+    /**
+     * @param string $methodName the name of the method to invoke
+     * @param Value[] $params array of parameters to be passed to the method (NB: Value objects, not plain php values)
+     */
+    public function __construct($methodName, $params = array())
+    {
+        $this->methodname = $methodName;
+        foreach ($params as $param) {
+            $this->addParam($param);
+        }
+    }
+
+    /**
+     * Gets/sets the xml-rpc method to be invoked.
+     *
+     * @param string $methodName the method to be set (leave empty not to set it)
+     * @return string the method that will be invoked
+     */
+    public function method($methodName = '')
+    {
+        if ($methodName != '') {
+            $this->methodname = $methodName;
+        }
+
+        return $this->methodname;
+    }
+
+    /**
+     * Add a parameter to the list of parameters to be used upon method invocation.
+     * Checks that $params is actually a Value object and not a plain php value.
+     *
+     * @param Value $param
+     * @return boolean false on failure
+     */
+    public function addParam($param)
+    {
+        // check: do not add to self params which are not xml-rpc values
+        if (is_object($param) && is_a($param, 'PhpXmlRpc\Value')) {
+            $this->params[] = $param;
+
+            return true;
+        } else {
+            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': value passed in must be a PhpXmlRpc\Value');
+            return false;
+        }
+    }
+
+    /**
+     * Returns the nth parameter in the request. The index zero-based.
+     *
+     * @param integer $i the index of the parameter to fetch (zero based)
+     * @return Value the i-th parameter
+     */
+    public function getParam($i)
+    {
+        return $this->params[$i];
+    }
+
+    /**
+     * Returns the number of parameters in the message.
+     *
+     * @return integer the number of parameters currently set
+     */
+    public function getNumParams()
+    {
+        return count($this->params);
+    }
+
+    /**
+     * Returns xml representation of the message, XML prologue included. Sets `payload` and `content_type` properties
+     *
+     * @param string $charsetEncoding
+     * @return string the xml representation of the message, xml prologue included
+     */
+    public function serialize($charsetEncoding = '')
+    {
+        $this->createPayload($charsetEncoding);
+
+        return $this->payload;
+    }
+
+    /**
+     * @internal this function will become protected in the future (and be folded into serialize)
+     *
+     * @param string $charsetEncoding
+     * @return void
+     */
+    public function createPayload($charsetEncoding = '')
+    {
+        $this->logDeprecationUnlessCalledBy('serialize');
+
+        if ($charsetEncoding != '') {
+            $this->content_type = 'text/xml; charset=' . $charsetEncoding;
+        } else {
+            $this->content_type = 'text/xml';
+        }
+
+        $result = $this->xml_header($charsetEncoding);
+        $result .= '<methodName>' . $this->getCharsetEncoder()->encodeEntities(
+                $this->methodname, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "</methodName>\n";
+        $result .= "<params>\n";
+        foreach ($this->params as $p) {
+            $result .= "<param>\n" . $p->serialize($charsetEncoding) .
+                "</param>\n";
+        }
+        $result .= "</params>\n";
+        $result .= $this->xml_footer();
+
+        $this->payload = $result;
+    }
+
+    /**
+     * @internal this function will become protected in the future (and be folded into serialize)
+     *
+     * @param string $charsetEncoding
+     * @return string
+     */
+    public function xml_header($charsetEncoding = '')
+    {
+        $this->logDeprecationUnlessCalledBy('createPayload');
+
+        if ($charsetEncoding != '') {
+            return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\" ?" . ">\n<methodCall>\n";
+        } else {
+            return "<?xml version=\"1.0\"?" . ">\n<methodCall>\n";
+        }
+    }
+
+    /**
+     * @internal this function will become protected in the future (and be folded into serialize)
+     *
+     * @return string
+     */
+    public function xml_footer()
+    {
+        $this->logDeprecationUnlessCalledBy('createPayload');
+
+        return '</methodCall>';
+    }
+
+    /**
+     * Given an open file handle, read all data available and parse it as an xml-rpc response.
+     *
+     * NB: the file handle is not closed by this function.
+     * NNB: might have trouble in rare cases to work on network streams, as we check for a read of 0 bytes instead of
+     *      feof($fp). But since checking for feof(null) returns false, we would risk an infinite loop in that case,
+     *      because we cannot trust the caller to give us a valid pointer to an open file...
+     *
+     * @param resource $fp stream pointer
+     * @param bool $headersProcessed
+     * @param string $returnType
+     * @return Response
+     *
+     * @todo arsing Responses is not really the responsibility of the Request class. Maybe of the Client...
+     * @todo feature creep - add a flag to disable trying to parse the http headers
+     */
+    public function parseResponseFile($fp, $headersProcessed = false, $returnType = 'xmlrpcvals')
+    {
+        $ipd = '';
+        // q: is there an optimal buffer size? Is there any value in making the buffer size a tuneable?
+        while ($data = fread($fp, 32768)) {
+            $ipd .= $data;
+        }
+        return $this->parseResponse($ipd, $headersProcessed, $returnType);
+    }
+
+    /**
+     * Parse the xml-rpc response contained in the string $data and return a Response object.
+     *
+     * When $this->debug has been set to a value greater than 0, will echo debug messages to screen while decoding.
+     *
+     * @param string $data the xml-rpc response, possibly including http headers
+     * @param bool $headersProcessed when true prevents parsing HTTP headers for interpretation of content-encoding and
+     *                               consequent decoding
+     * @param string $returnType decides return type, i.e. content of response->value(). Either 'xmlrpcvals', 'xml' or
+     *                           'phpvals'
+     * @return Response
+     *
+     * @todo parsing Responses is not really the responsibility of the Request class. Maybe of the Client...
+     * @todo what about only populating 'raw_data' in httpResponse when debug mode is > 0?
+     * @todo feature creep - allow parsing data gotten from a stream pointer instead of a string: read it piecewise,
+     *       looking first for separation between headers and body, then for charset indicators, server debug info and
+     *       </methodResponse>. That would require a notable increase in code complexity...
+     */
+    public function parseResponse($data = '', $headersProcessed = false, $returnType = XMLParser::RETURN_XMLRPCVALS)
+    {
+        if ($this->debug > 0) {
+            $this->getLogger()->debug("---GOT---\n$data\n---END---");
+        }
+
+        $this->httpResponse = array('raw_data' => $data, 'headers' => array(), 'cookies' => array());
+
+        if ($data == '') {
+            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': no response received from server.');
+            return new Response(0, PhpXmlRpc::$xmlrpcerr['no_data'], PhpXmlRpc::$xmlrpcstr['no_data']);
+        }
+
+        // parse the HTTP headers of the response, if present, and separate them from data
+        if (substr($data, 0, 4) == 'HTTP') {
+            $httpParser = new Http();
+            try {
+                $httpResponse = $httpParser->parseResponseHeaders($data, $headersProcessed, $this->debug > 0);
+            } catch (HttpException $e) {
+                // failed processing of HTTP response headers
+                // save into response obj the full payload received, for debugging
+                return new Response(0, $e->getCode(), $e->getMessage(), '', array('raw_data' => $data, 'status_code', $e->statusCode()));
+            } catch(\Exception $e) {
+                return new Response(0, $e->getCode(), $e->getMessage(), '', array('raw_data' => $data));
+            }
+        } else {
+            $httpResponse = $this->httpResponse;
+        }
+
+        // be tolerant of extra whitespace in response body
+        $data = trim($data);
+
+        /// @todo optimization creep - return an error msg if $data == ''
+
+        // be tolerant of junk after methodResponse (e.g. javascript ads automatically inserted by free hosts)
+        // idea from Luca Mariano, originally in PEARified version of the lib
+        $pos = strrpos($data, '</methodResponse>');
+        if ($pos !== false) {
+            $data = substr($data, 0, $pos + 17);
+        }
+
+        // try to 'guestimate' the character encoding of the received response
+        $respEncoding = XMLParser::guessEncoding(
+            isset($httpResponse['headers']['content-type']) ? $httpResponse['headers']['content-type'] : '',
+            $data
+        );
+
+        if ($this->debug >= 0) {
+            $this->httpResponse = $httpResponse;
+        } else {
+            $httpResponse = null;
+        }
+
+        if ($this->debug > 0) {
+            $start = strpos($data, '<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
+            if ($start) {
+                $start += strlen('<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
+                /// @todo what if there is no end tag?
+                $end = strpos($data, '-->', $start);
+                $comments = substr($data, $start, $end - $start);
+                $this->getLogger()->debug("---SERVER DEBUG INFO (DECODED)---\n\t" .
+                    str_replace("\n", "\n\t", base64_decode($comments)) . "\n---END---", array('encoding' => $respEncoding));
+            }
+        }
+
+        // if the user wants back raw xml, give it to her
+        if ($returnType == 'xml') {
+            return new Response($data, 0, '', 'xml', $httpResponse);
+        }
+
+        /// @todo move this block of code into the XMLParser
+        if ($respEncoding != '') {
+            // Since parsing will fail if charset is not specified in the xml declaration,
+            // the encoding is not UTF8 and there are non-ascii chars in the text, we try to work round that...
+            // The following code might be better for mb_string enabled installs, but makes the lib about 200% slower...
+            //if (!is_valid_charset($respEncoding, array('UTF-8')))
+            if (!in_array($respEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) {
+                if (function_exists('mb_convert_encoding')) {
+                    $data = mb_convert_encoding($data, 'UTF-8', $respEncoding);
+                } else {
+                    if ($respEncoding == 'ISO-8859-1') {
+                        $data = utf8_encode($data);
+                    } else {
+                        $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': unsupported charset encoding of received response: ' . $respEncoding);
+                    }
+                }
+            }
+        }
+        // PHP internally might use ISO-8859-1, so we have to tell the xml parser to give us back data in the expected charset.
+        // What if internal encoding is not in one of the 3 allowed? We use the broadest one, i.e. utf8
+        if (in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
+            $options = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
+        } else {
+            $options = array(XML_OPTION_TARGET_ENCODING => 'UTF-8', 'target_charset' => PhpXmlRpc::$xmlrpc_internalencoding);
+        }
+
+        $xmlRpcParser = $this->getParser();
+        $_xh = $xmlRpcParser->parse($data, $returnType, XMLParser::ACCEPT_RESPONSE, $options);
+        // BC
+        if (!is_array($_xh)) {
+            $_xh = $xmlRpcParser->_xh;
+        }
+
+        // first error check: xml not well-formed
+        if ($_xh['isf'] == 3) {
+
+            // BC break: in the past for some cases we used the error message: 'XML error at line 1, check URL'
+
+            // Q: should we give back an error with variable error number, as we do server-side? But if we do, will
+            //    we be able to tell apart the two cases? In theory, we never emit invalid xml on our end, but
+            //    there could be proxies meddling with the request, or network data corruption...
+
+            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_xml'],
+                PhpXmlRpc::$xmlrpcstr['invalid_xml'] . ' ' . $_xh['isf_reason'], '', $httpResponse);
+
+            if ($this->debug > 0) {
+                $this->getLogger()->debug($_xh['isf_reason']);
+            }
+        }
+        // second error check: xml well-formed but not xml-rpc compliant
+        elseif ($_xh['isf'] == 2) {
+            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['xml_not_compliant'],
+                PhpXmlRpc::$xmlrpcstr['xml_not_compliant'] . ' ' . $_xh['isf_reason'], '', $httpResponse);
+
+            /// @todo echo something for the user? check if it was already done by the parser...
+            //if ($this->debug > 0) {
+            //    $this->getLogger()->debug($_xh['isf_reason']);
+            //}
+        }
+        // third error check: parsing of the response has somehow gone boink.
+        /// @todo shall we omit this check, since we trust the parsing code?
+        elseif ($_xh['isf'] > 3 || ($returnType == XMLParser::RETURN_XMLRPCVALS && !is_object($_xh['value']))) {
+            // something odd has happened and it's time to generate a client side error indicating something odd went on
+            $r = new Response(0, PhpXmlRpc::$xmlrpcerr['xml_parsing_error'], PhpXmlRpc::$xmlrpcstr['xml_parsing_error'],
+                '', $httpResponse
+            );
+
+            /// @todo echo something for the user?
+        } else {
+            if ($this->debug > 1) {
+                $this->getLogger()->debug(
+                    "---PARSED---\n".var_export($_xh['value'], true)."\n---END---"
+                );
+            }
+
+            $v = $_xh['value'];
+
+            if ($_xh['isf']) {
+                /// @todo we should test (here or preferably in the parser) if server sent an int and a string, and/or
+                ///       coerce them into such...
+                if ($returnType == XMLParser::RETURN_XMLRPCVALS) {
+                    $errNo_v = $v['faultCode'];
+                    $errStr_v = $v['faultString'];
+                    $errNo = $errNo_v->scalarVal();
+                    $errStr = $errStr_v->scalarVal();
+                } else {
+                    $errNo = $v['faultCode'];
+                    $errStr = $v['faultString'];
+                }
+
+                if ($errNo == 0) {
+                    // FAULT returned, errno needs to reflect that
+                    /// @todo feature creep - add this code to PhpXmlRpc::$xmlrpcerr
+                    $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': fault response received with faultCode 0 or null. Converted it to -1');
+                    /// @todo in Encoder::decodeXML, we use PhpXmlRpc::$xmlrpcerr['invalid_return'] for this case (see
+                    ///       also the todo 17 lines above)
+                    $errNo = -1;
+                }
+
+                $r = new Response(0, $errNo, $errStr, '', $httpResponse);
+            } else {
+                $r = new Response($v, 0, '', $returnType, $httpResponse);
+            }
+        }
+
+        return $r;
+    }
+
+    /**
+     * Kept the old name even if Request class was renamed, for BC.
+     *
+     * @return string
+     */
+    public function kindOf()
+    {
+        return 'msg';
+    }
+
+    /**
+     * Enables/disables the echoing to screen of the xml-rpc responses received.
+     *
+     * @param integer $level values <0, 0, 1, >1 are supported
+     * @return $this
+     */
+    public function setDebug($level)
+    {
+        $this->debug = $level;
+        return $this;
+    }
+
+    // *** BC layer ***
+
+    // we have to make this return by ref in order to allow calls such as `$resp->_cookies['name'] = ['value' => 'something'];`
+    public function &__get($name)
+    {
+        switch ($name) {
+            case 'me':
+            case 'mytype':
+            case '_php_class':
+            case 'payload':
+            case 'content_type':
+                $this->logDeprecation('Getting property Request::' . $name . ' is deprecated');
+                return $this->$name;
+            case 'httpResponse':
+                // manually implement the 'protected property' behaviour
+                $canAccess = false;
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+                if (isset($trace[1]) && isset($trace[1]['class'])) {
+                    if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Request')) {
+                        $canAccess = true;
+                    }
+                }
+                if ($canAccess) {
+                    $this->logDeprecation('Getting property Request::' . $name . ' is deprecated');
+                    return $this->httpResponse;
+                } else {
+                    trigger_error("Cannot access protected property Request::httpResponse in " . __FILE__, E_USER_ERROR);
+                }
+                break;
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+                $result = null;
+                return $result;
+        }
+    }
+
+    public function __set($name, $value)
+    {
+        switch ($name) {
+            case 'methodname':
+            case 'params':
+            case 'debug':
+            case 'payload':
+            case 'content_type':
+                $this->logDeprecation('Setting property Request::' . $name . ' is deprecated');
+                $this->$name = $value;
+                break;
+            case 'httpResponse':
+                // manually implement the 'protected property' behaviour
+                $canAccess = false;
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+                if (isset($trace[1]) && isset($trace[1]['class'])) {
+                    if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Request')) {
+                        $canAccess = true;
+                    }
+                }
+                if ($canAccess) {
+                    $this->logDeprecation('Setting property Request::' . $name . ' is deprecated');
+                    $this->httpResponse = $value;
+                } else {
+                    trigger_error("Cannot access protected property Request::httpResponse in " . __FILE__, E_USER_ERROR);
+                }
+                break;
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __set(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+        }
+    }
+
+    public function __isset($name)
+    {
+        switch ($name) {
+            case 'methodname':
+            case 'params':
+            case 'debug':
+            case 'payload':
+            case 'content_type':
+                $this->logDeprecation('Checking property Request::' . $name . ' is deprecated');
+                return isset($this->$name);
+            case 'httpResponse':
+                // manually implement the 'protected property' behaviour
+                $canAccess = false;
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+                if (isset($trace[1]) && isset($trace[1]['class'])) {
+                    if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Request')) {
+                        $canAccess = true;
+                    }
+                }
+                if ($canAccess) {
+                    $this->logDeprecation('Checking property Request::' . $name . ' is deprecated');
+                    return isset($this->httpResponse);
+                }
+                // break through voluntarily
+            default:
+                return false;
+        }
+    }
+
+    public function __unset($name)
+    {
+        switch ($name) {
+            case 'methodname':
+            case 'params':
+            case 'debug':
+            case 'payload':
+            case 'content_type':
+                $this->logDeprecation('Unsetting property Request::' . $name . ' is deprecated');
+                unset($this->$name);
+                break;
+            case 'httpResponse':
+                // manually implement the 'protected property' behaviour
+                $canAccess = false;
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+                if (isset($trace[1]) && isset($trace[1]['class'])) {
+                    if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Request')) {
+                        $canAccess = true;
+                    }
+                }
+                if ($canAccess) {
+                    $this->logDeprecation('Unsetting property Request::' . $name . ' is deprecated');
+                    unset($this->httpResponse);
+                } else {
+                    trigger_error("Cannot access protected property Request::httpResponse in " . __FILE__, E_USER_ERROR);
+                }
+                break;
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __unset(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+        }
+    }
+}

+ 338 - 0
includes/phpxmlrpc/Response.php

@@ -0,0 +1,338 @@
+<?php
+
+namespace PhpXmlRpc;
+
+use PhpXmlRpc\Exception\StateErrorException;
+use PhpXmlRpc\Traits\CharsetEncoderAware;
+use PhpXmlRpc\Traits\DeprecationLogger;
+use PhpXmlRpc\Traits\PayloadBearer;
+
+/**
+ * This class provides the representation of the response of an XML-RPC server.
+ * Server-side, a server method handler will construct a Response and pass it as its return value.
+ * An identical Response object will be returned by the result of an invocation of the send() method of the Client class.
+ *
+ * @property Value|string|mixed $val deprecated - public access left in purely for BC. Access via value()/__construct()
+ * @property string $valtyp deprecated - public access left in purely for BC. Access via valueType()/__construct()
+ * @property int $errno deprecated - public access left in purely for BC. Access via faultCode()/__construct()
+ * @property string $errstr deprecated - public access left in purely for BC. Access faultString()/__construct()
+ * @property string $payload deprecated - public access left in purely for BC. Access via getPayload()/setPayload()
+ * @property string $content_type deprecated - public access left in purely for BC. Access via getContentType()/setPayload()
+ * @property array $hdrs deprecated. Access via httpResponse()['headers'], set via $httpResponse['headers']
+ * @property array _cookies deprecated. Access via httpResponse()['cookies'], set via $httpResponse['cookies']
+ * @property string $raw_data deprecated. Access via httpResponse()['raw_data'], set via $httpResponse['raw_data']
+ */
+class Response
+{
+    use CharsetEncoderAware;
+    use DeprecationLogger;
+    use PayloadBearer;
+
+    /** @var Value|string|mixed */
+    protected $val = 0;
+    /** @var string */
+    protected $valtyp;
+    /** @var int */
+    protected $errno = 0;
+    /** @var string */
+    protected $errstr = '';
+
+    protected $httpResponse = array('headers' => array(), 'cookies' => array(), 'raw_data' => '', 'status_code' => null);
+
+    /**
+     * @param Value|string|mixed $val either a Value object, a php value or the xml serialization of an xml-rpc value (a string).
+     *                                Note that using anything other than a Value object wll have an impact on serialization.
+     * @param integer $fCode set it to anything but 0 to create an error response. In that case, $val is discarded
+     * @param string $fString the error string, in case of an error response
+     * @param string $valType The type of $val passed in. Either 'xmlrpcvals', 'phpvals' or 'xml'. Leave empty to let
+     *                        the code guess the correct type by looking at $val - in which case strings are assumed
+     *                        to be serialized xml
+     * @param array|null $httpResponse this should be set when the response is being built out of data received from
+     *                                 http (i.e. not when programmatically building a Response server-side). Array
+     *                                 keys should include, if known: headers, cookies, raw_data, status_code
+     *
+     * @todo add check that $val / $fCode / $fString is of correct type? We could at least log a warning for fishy cases...
+     *       NB: as of now we do not do it, since it might be either an xml-rpc value or a plain php val, or a complete
+     *       xml chunk, depending on usage of Client::send() inside which the constructor is called.
+     */
+    public function __construct($val, $fCode = 0, $fString = '', $valType = '', $httpResponse = null)
+    {
+        if ($fCode != 0) {
+            // error response
+            $this->errno = $fCode;
+            $this->errstr = $fString;
+        } else {
+            // successful response
+            $this->val = $val;
+            if ($valType == '') {
+                // user did not declare type of response value: try to guess it
+                if (is_object($this->val) && is_a($this->val, 'PhpXmlRpc\Value')) {
+                    $this->valtyp = 'xmlrpcvals';
+                } elseif (is_string($this->val)) {
+                    $this->valtyp = 'xml';
+                } else {
+                    $this->valtyp = 'phpvals';
+                }
+            } else {
+                $this->valtyp = $valType;
+                // user declares the type of resp value: we "almost" trust it... but log errors just in case
+                if (($this->valtyp == 'xmlrpcvals' && (!is_a($this->val, 'PhpXmlRpc\Value'))) ||
+                    ($this->valtyp == 'xml' && (!is_string($this->val)))) {
+                    $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': value passed in does not match type ' . $valType);
+                }
+            }
+        }
+
+        if (is_array($httpResponse)) {
+            $this->httpResponse = array_merge(array('headers' => array(), 'cookies' => array(), 'raw_data' => '', 'status_code' => null), $httpResponse);
+        }
+    }
+
+    /**
+     * Returns the error code of the response.
+     *
+     * @return integer the error code of this response (0 for not-error responses)
+     */
+    public function faultCode()
+    {
+        return $this->errno;
+    }
+
+    /**
+     * Returns the error code of the response.
+     *
+     * @return string the error string of this response ('' for not-error responses)
+     */
+    public function faultString()
+    {
+        return $this->errstr;
+    }
+
+    /**
+     * Returns the value received by the server. If the Response's faultCode is non-zero then the value returned by this
+     * method should not be used (it may not even be an object).
+     *
+     * @return Value|string|mixed the Value object returned by the server. Might be an xml string or plain php value
+     *                            depending on the convention adopted when creating the Response
+     */
+    public function value()
+    {
+        return $this->val;
+    }
+
+    /**
+     * @return string
+     */
+    public function valueType()
+    {
+        return $this->valtyp;
+    }
+
+    /**
+     * Returns an array with the cookies received from the server.
+     * Array has the form: $cookiename => array ('value' => $val, $attr1 => $val1, $attr2 => $val2, ...)
+     * with attributes being e.g. 'expires', 'path', domain'.
+     * NB: cookies sent as 'expired' by the server (i.e. with an expiry date in the past) are still present in the array.
+     * It is up to the user-defined code to decide how to use the received cookies, and whether they have to be sent back
+     * with the next request to the server (using $client->setCookie) or not.
+     * The values are filled in at constructor time, and might not be set for specific debug values used.
+     *
+     * @return array[] array of cookies received from the server
+     */
+    public function cookies()
+    {
+        return $this->httpResponse['cookies'];
+    }
+
+    /**
+     * Returns an array with info about the http response received from the server.
+     * The values are filled in at constructor time, and might not be set for specific debug values used.
+     *
+     * @return array array with keys 'headers', 'cookies', 'raw_data' and 'status_code'.
+     */
+    public function httpResponse()
+    {
+        return $this->httpResponse;
+    }
+
+    /**
+     * Returns xml representation of the response, XML prologue _not_ included. Sets `payload` and `content_type` properties
+     *
+     * @param string $charsetEncoding the charset to be used for serialization. If null, US-ASCII is assumed
+     * @return string the xml representation of the response
+     * @throws StateErrorException if the response was built out of a value of an unsupported type
+     */
+    public function serialize($charsetEncoding = '')
+    {
+        if ($charsetEncoding != '') {
+            $this->content_type = 'text/xml; charset=' . $charsetEncoding;
+        } else {
+            $this->content_type = 'text/xml';
+        }
+
+        if (PhpXmlRpc::$xmlrpc_null_apache_encoding) {
+            $result = "<methodResponse xmlns:ex=\"" . PhpXmlRpc::$xmlrpc_null_apache_encoding_ns . "\">\n";
+        } else {
+            $result = "<methodResponse>\n";
+        }
+        if ($this->errno) {
+            // Let non-ASCII response messages be tolerated by clients by xml-encoding non ascii chars
+            $result .= "<fault>\n" .
+                "<value>\n<struct><member><name>faultCode</name>\n<value><int>" . $this->errno .
+                "</int></value>\n</member>\n<member>\n<name>faultString</name>\n<value><string>" .
+                $this->getCharsetEncoder()->encodeEntities($this->errstr, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) .
+                "</string></value>\n</member>\n</struct>\n</value>\n</fault>";
+        } else {
+            if (is_object($this->val) && is_a($this->val, 'PhpXmlRpc\Value')) {
+                $result .= "<params>\n<param>\n" . $this->val->serialize($charsetEncoding) . "</param>\n</params>";
+            } else if (is_string($this->val) && $this->valtyp == 'xml') {
+                $result .= "<params>\n<param>\n" .
+                    $this->val .
+                    "</param>\n</params>";
+            } else if ($this->valtyp == 'phpvals') {
+                    $encoder = new Encoder();
+                    $val = $encoder->encode($this->val);
+                    $result .= "<params>\n<param>\n" . $val->serialize($charsetEncoding) . "</param>\n</params>";
+            } else {
+                throw new StateErrorException('cannot serialize xmlrpc response objects whose content is native php values');
+            }
+        }
+        $result .= "\n</methodResponse>";
+
+        $this->payload = $result;
+
+        return $result;
+    }
+
+    /**
+     * @param string $charsetEncoding
+     * @return string
+     */
+    public function xml_header($charsetEncoding = '')
+    {
+        if ($charsetEncoding != '') {
+            return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\"?" . ">\n";
+        } else {
+            return "<?xml version=\"1.0\"?" . ">\n";
+        }
+    }
+
+    // *** BC layer ***
+
+    // we have to make this return by ref in order to allow calls such as `$resp->_cookies['name'] = ['value' => 'something'];`
+    public function &__get($name)
+    {
+        switch ($name) {
+            case 'val':
+            case 'valtyp':
+            case 'errno':
+            case 'errstr':
+            case 'payload':
+            case 'content_type':
+                $this->logDeprecation('Getting property Response::' . $name . ' is deprecated');
+                return $this->$name;
+            case 'hdrs':
+                $this->logDeprecation('Getting property Response::' . $name . ' is deprecated');
+                return $this->httpResponse['headers'];
+            case '_cookies':
+                $this->logDeprecation('Getting property Response::' . $name . ' is deprecated');
+                return $this->httpResponse['cookies'];
+            case 'raw_data':
+                $this->logDeprecation('Getting property Response::' . $name . ' is deprecated');
+                return $this->httpResponse['raw_data'];
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+                $result = null;
+                return $result;
+        }
+    }
+
+    public function __set($name, $value)
+    {
+        switch ($name) {
+            case 'val':
+            case 'valtyp':
+            case 'errno':
+            case 'errstr':
+            case 'payload':
+            case 'content_type':
+                $this->logDeprecation('Setting property Response::' . $name . ' is deprecated');
+                $this->$name = $value;
+                break;
+            case 'hdrs':
+                $this->logDeprecation('Setting property Response::' . $name . ' is deprecated');
+                $this->httpResponse['headers'] = $value;
+                break;
+            case '_cookies':
+                $this->logDeprecation('Setting property Response::' . $name . ' is deprecated');
+                $this->httpResponse['cookies'] = $value;
+                break;
+            case 'raw_data':
+                $this->logDeprecation('Setting property Response::' . $name . ' is deprecated');
+                $this->httpResponse['raw_data'] = $value;
+                break;
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __set(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+        }
+    }
+
+    public function __isset($name)
+    {
+        switch ($name) {
+            case 'val':
+            case 'valtyp':
+            case 'errno':
+            case 'errstr':
+            case 'payload':
+            case 'content_type':
+                $this->logDeprecation('Checking property Response::' . $name . ' is deprecated');
+                return isset($this->$name);
+            case 'hdrs':
+                $this->logDeprecation('Checking property Response::' . $name . ' is deprecated');
+                return isset($this->httpResponse['headers']);
+            case '_cookies':
+                $this->logDeprecation('Checking property Response::' . $name . ' is deprecated');
+                return isset($this->httpResponse['cookies']);
+            case 'raw_data':
+                $this->logDeprecation('Checking property Response::' . $name . ' is deprecated');
+                return isset($this->httpResponse['raw_data']);
+            default:
+                return false;
+        }
+    }
+
+    public function __unset($name)
+    {
+        switch ($name) {
+            case 'val':
+            case 'valtyp':
+            case 'errno':
+            case 'errstr':
+            case 'payload':
+            case 'content_type':
+                $this->logDeprecation('Setting property Response::' . $name . ' is deprecated');
+                unset($this->$name);
+                break;
+            case 'hdrs':
+                $this->logDeprecation('Unsetting property Response::' . $name . ' is deprecated');
+                unset($this->httpResponse['headers']);
+                break;
+            case '_cookies':
+                $this->logDeprecation('Unsetting property Response::' . $name . ' is deprecated');
+                unset($this->httpResponse['cookies']);
+                break;
+            case 'raw_data':
+                $this->logDeprecation('Unsetting property Response::' . $name . ' is deprecated');
+                unset($this->httpResponse['raw_data']);
+                break;
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __unset(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+        }
+    }
+}

+ 1612 - 0
includes/phpxmlrpc/Server.php

@@ -0,0 +1,1612 @@
+<?php
+
+namespace PhpXmlRpc;
+
+use PhpXmlRpc\Exception\NoSuchMethodException;
+use PhpXmlRpc\Exception\ValueErrorException;
+use PhpXmlRpc\Helper\Http;
+use PhpXmlRpc\Helper\Interop;
+use PhpXmlRpc\Helper\Logger;
+use PhpXmlRpc\Helper\XMLParser;
+use PhpXmlRpc\Traits\CharsetEncoderAware;
+use PhpXmlRpc\Traits\DeprecationLogger;
+use PhpXmlRpc\Traits\ParserAware;
+
+/**
+ * Allows effortless implementation of XML-RPC servers
+ *
+ * @property string[] $accepted_compression deprecated - public access left in purely for BC. Access via getOption()/setOption()
+ * @property bool $allow_system_funcs deprecated - public access left in purely for BC. Access via getOption()/setOption()
+ * @property bool $compress_response deprecated - public access left in purely for BC. Access via getOption()/setOption()
+ * @property int $debug deprecated - public access left in purely for BC. Access via getOption()/setOption()
+ * @property int $exception_handling deprecated - public access left in purely for BC. Access via getOption()/setOption()
+ * @property string $functions_parameters_type deprecated - public access left in purely for BC. Access via getOption()/setOption()
+ * @property array $phpvals_encoding_options deprecated - public access left in purely for BC. Access via getOption()/setOption()
+ * @property string $response_charset_encoding deprecated - public access left in purely for BC. Access via getOption()/setOption()
+ */
+class Server
+{
+    use CharsetEncoderAware;
+    use DeprecationLogger;
+    use ParserAware;
+
+    const OPT_ACCEPTED_COMPRESSION = 'accepted_compression';
+    const OPT_ALLOW_SYSTEM_FUNCS = 'allow_system_funcs';
+    const OPT_COMPRESS_RESPONSE = 'compress_response';
+    const OPT_DEBUG = 'debug';
+    const OPT_EXCEPTION_HANDLING = 'exception_handling';
+    const OPT_FUNCTIONS_PARAMETERS_TYPE = 'functions_parameters_type';
+    const OPT_PHPVALS_ENCODING_OPTIONS = 'phpvals_encoding_options';
+    const OPT_RESPONSE_CHARSET_ENCODING = 'response_charset_encoding';
+
+    /** @var string */
+    protected static $responseClass = '\\PhpXmlRpc\\Response';
+
+    /**
+     * @var string
+     * Defines how functions in $dmap will be invoked: either using an xml-rpc Request object or plain php values.
+     * Valid strings are 'xmlrpcvals', 'phpvals' or 'epivals' (only for use by polyfill-xmlrpc).
+     *
+     * @todo create class constants for these
+     */
+    protected $functions_parameters_type = 'xmlrpcvals';
+
+    /**
+     * @var array
+     * Option used for fine-tuning the encoding the php values returned from functions registered in the dispatch map
+     * when the functions_parameters_type member is set to 'phpvals'.
+     * @see Encoder::encode for a list of values
+     */
+    protected $phpvals_encoding_options = array('auto_dates');
+
+    /**
+     * @var int
+     * Controls whether the server is going to echo debugging messages back to the client as comments in response body.
+     * SECURITY SENSITIVE!
+     * Valid values:
+     * 0 =
+     * 1 =
+     * 2 =
+     * 3 =
+     */
+    protected $debug = 1;
+
+    /**
+     * @var int
+     * Controls behaviour of server when the invoked method-handler function throws an exception (within the `execute` method):
+     * 0 = catch it and return an 'internal error' xml-rpc response (default)
+     * 1 = SECURITY SENSITIVE DO NOT ENABLE ON PUBLIC SERVERS!!! catch it and return an xml-rpc response with the error
+     *     corresponding to the exception, both its code and message.
+     * 2 = allow the exception to float to the upper layers
+     * Can be overridden per-method-handler in the dispatch map
+     */
+    protected $exception_handling = 0;
+
+    /**
+     * @var bool
+     * When set to true, it will enable HTTP compression of the response, in case the client has declared its support
+     * for compression in the request.
+     * Automatically set at constructor time.
+     */
+    protected $compress_response = false;
+
+    /**
+     * @var string[]
+     * List of http compression methods accepted by the server for requests. Automatically set at constructor time.
+     * NB: PHP supports deflate, gzip compressions out of the box if compiled w. zlib
+     */
+    protected $accepted_compression = array();
+
+    /**
+     * @var bool
+     * Shall we serve calls to system.* methods?
+     */
+    protected $allow_system_funcs = true;
+
+    /**
+     * List of charset encodings natively accepted for requests.
+     * Set at constructor time.
+     * @deprecated UNUSED so far by this library. It is still accessible by subclasses but will be dropped in the future.
+     */
+    private $accepted_charset_encodings = array();
+
+    /**
+     * @var string
+     * Charset encoding to be used for response.
+     * NB: if we can, we will convert the generated response from internal_encoding to the intended one.
+     * Can be:
+     * - a supported xml encoding (only UTF-8 and ISO-8859-1, unless mbstring is enabled),
+     * - null (leave unspecified in response, convert output stream to US_ASCII),
+     * - 'auto' (use client-specified charset encoding or same as request if request headers do not specify it (unless request is US-ASCII: then use library default anyway).
+     * NB: pretty dangerous if you accept every charset and do not have mbstring enabled)
+     */
+    protected $response_charset_encoding = '';
+
+    protected static $options = array(
+        self::OPT_ACCEPTED_COMPRESSION,
+        self::OPT_ALLOW_SYSTEM_FUNCS,
+        self::OPT_COMPRESS_RESPONSE,
+        self::OPT_DEBUG,
+        self::OPT_EXCEPTION_HANDLING,
+        self::OPT_FUNCTIONS_PARAMETERS_TYPE,
+        self::OPT_PHPVALS_ENCODING_OPTIONS,
+        self::OPT_RESPONSE_CHARSET_ENCODING,
+    );
+
+    /**
+     * @var mixed
+     * Extra data passed at runtime to method handling functions. Used only by EPI layer
+     * @internal
+     */
+    public $user_data = null;
+
+    /**
+     * Array defining php functions exposed as xml-rpc methods by this server.
+     * @var array[] $dmap
+     */
+    protected $dmap = array();
+
+    /**
+     * Storage for internal debug info.
+     */
+    protected $debug_info = '';
+
+    protected static $_xmlrpc_debuginfo = '';
+    protected static $_xmlrpcs_occurred_errors = '';
+    protected static $_xmlrpcs_prev_ehandler = '';
+
+    /**
+     * @param array[] $dispatchMap the dispatch map with definition of exposed services
+     *                             Array keys are the names of the method names.
+     *                             Each array value is an array with the following members:
+     *                             - function (callable)
+     *                             - docstring (optional)
+     *                             - signature (array, optional)
+     *                             - signature_docs (array, optional)
+     *                             - parameters_type (string, optional)
+     *                             - exception_handling (int, optional)
+     * @param boolean $serviceNow set to false in order to prevent the server from running upon construction
+     */
+    public function __construct($dispatchMap = null, $serviceNow = true)
+    {
+        // if ZLIB is enabled, let the server by default accept compressed requests,
+        // and compress responses sent to clients that support them
+        if (function_exists('gzinflate')) {
+            $this->accepted_compression[] = 'gzip';
+        }
+        if (function_exists('gzuncompress')) {
+            $this->accepted_compression[] = 'deflate';
+        }
+        if (function_exists('gzencode') || function_exists('gzcompress')) {
+            $this->compress_response = true;
+        }
+
+        // by default the xml parser can support these 3 charset encodings
+        $this->accepted_charset_encodings = array('UTF-8', 'ISO-8859-1', 'US-ASCII');
+
+        // dispMap is a dispatch array of methods mapped to function names and signatures.
+        // If a method doesn't appear in the map then an unknown method error is generated.
+        // milosch - changed to make passing dispMap optional. Instead, you can use the addToMap() function
+        // to add functions manually (borrowed from SOAPX4)
+        if ($dispatchMap) {
+            $this->setDispatchMap($dispatchMap);
+            if ($serviceNow) {
+                $this->service();
+            }
+        }
+    }
+
+    /**
+     * @param string $name see all the OPT_ constants
+     * @param mixed $value
+     * @return $this
+     * @throws ValueErrorException on unsupported option
+     */
+    public function setOption($name, $value)
+    {
+        switch ($name) {
+            case self::OPT_ACCEPTED_COMPRESSION :
+            case self::OPT_ALLOW_SYSTEM_FUNCS:
+            case self::OPT_COMPRESS_RESPONSE:
+            case self::OPT_DEBUG:
+            case self::OPT_EXCEPTION_HANDLING:
+            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
+            case self::OPT_PHPVALS_ENCODING_OPTIONS:
+            case self::OPT_RESPONSE_CHARSET_ENCODING:
+                $this->$name = $value;
+                break;
+            default:
+                throw new ValueErrorException("Unsupported option '$name'");
+        }
+
+        return $this;
+    }
+
+    /**
+     * @param string $name see all the OPT_ constants
+     * @return mixed
+     * @throws ValueErrorException on unsupported option
+     */
+    public function getOption($name)
+    {
+        switch ($name) {
+            case self::OPT_ACCEPTED_COMPRESSION:
+            case self::OPT_ALLOW_SYSTEM_FUNCS:
+            case self::OPT_COMPRESS_RESPONSE:
+            case self::OPT_DEBUG:
+            case self::OPT_EXCEPTION_HANDLING:
+            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
+            case self::OPT_PHPVALS_ENCODING_OPTIONS:
+            case self::OPT_RESPONSE_CHARSET_ENCODING:
+                return $this->$name;
+            default:
+                throw new ValueErrorException("Unsupported option '$name'");
+        }
+    }
+
+    /**
+     * Returns the complete list of Server options.
+     * @return array
+     */
+    public function getOptions()
+    {
+        $values = array();
+        foreach(static::$options as $opt) {
+            $values[$opt] = $this->getOption($opt);
+        }
+        return $values;
+    }
+
+    /**
+     * @param array $options key:  see all the OPT_ constants
+     * @return $this
+     * @throws ValueErrorException on unsupported option
+     */
+    public function setOptions($options)
+    {
+        foreach($options as $name => $value) {
+            $this->setOption($name, $value);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Set debug level of server.
+     *
+     * @param integer $level debug lvl: determines info added to xml-rpc responses (as xml comments)
+     *                    0 = no debug info,
+     *                    1 = msgs set from user with debugmsg(),
+     *                    2 = add complete xml-rpc request (headers and body),
+     *                    3 = add also all processing warnings happened during method processing
+     *                    (NB: this involves setting a custom error handler, and might interfere
+     *                    with the standard processing of the php function exposed as method. In
+     *                    particular, triggering a USER_ERROR level error will not halt script
+     *                    execution anymore, but just end up logged in the xml-rpc response)
+     *                    Note that info added at level 2 and 3 will be base64 encoded
+     * @return $this
+     */
+    public function setDebug($level)
+    {
+        $this->debug = $level;
+        return $this;
+    }
+
+    /**
+     * Add a string to the debug info that can be later serialized by the server as part of the response message.
+     * Note that for best compatibility, the debug string should be encoded using the PhpXmlRpc::$xmlrpc_internalencoding
+     * character set.
+     *
+     * @param string $msg
+     * @return void
+     */
+    public static function xmlrpc_debugmsg($msg)
+    {
+        static::$_xmlrpc_debuginfo .= $msg . "\n";
+    }
+
+    /**
+     * Add a string to the debug info that will be later serialized by the server as part of the response message
+     * (base64 encoded) when debug level >= 2
+     *
+     * @param string $msg
+     * @return void
+     */
+    public static function error_occurred($msg)
+    {
+        static::$_xmlrpcs_occurred_errors .= $msg . "\n";
+    }
+
+    /**
+     * Return a string with the serialized representation of all debug info.
+     *
+     * @internal this function will become protected in the future
+     *
+     * @param string $charsetEncoding the target charset encoding for the serialization
+     *
+     * @return string an XML comment (or two)
+     */
+    public function serializeDebug($charsetEncoding = '')
+    {
+        // Tough encoding problem: which internal charset should we assume for debug info?
+        // It might contain a copy of raw data received from client, ie with unknown encoding,
+        // intermixed with php generated data and user generated data...
+        // so we split it: system debug is base 64 encoded,
+        // user debug info should be encoded by the end user using the INTERNAL_ENCODING
+        $out = '';
+        if ($this->debug_info != '') {
+            $out .= "<!-- SERVER DEBUG INFO (BASE64 ENCODED):\n" . base64_encode($this->debug_info) . "\n-->\n";
+        }
+        if (static::$_xmlrpc_debuginfo != '') {
+            $out .= "<!-- DEBUG INFO:\n" . $this->getCharsetEncoder()->encodeEntities(str_replace('--', '_-', static::$_xmlrpc_debuginfo), PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "\n-->\n";
+            // NB: a better solution MIGHT be to use CDATA, but we need to insert it
+            // into return payload AFTER the beginning tag
+            //$out .= "<![CDATA[ DEBUG INFO:\n\n" . str_replace(']]>', ']_]_>', static::$_xmlrpc_debuginfo) . "\n]]>\n";
+        }
+
+        return $out;
+    }
+
+    /**
+     * Execute the xml-rpc request, printing the response.
+     *
+     * @param string $data the request body. If null, the http POST request will be examined
+     * @param bool $returnPayload When true, return the response but do not echo it or any http header
+     *
+     * @return Response|string the response object (usually not used by caller...) or its xml serialization
+     * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
+     */
+    public function service($data = null, $returnPayload = false)
+    {
+        if ($data === null) {
+            $data = file_get_contents('php://input');
+        }
+        $rawData = $data;
+
+        // reset internal debug info
+        $this->debug_info = '';
+
+        // Save what we received, before parsing it
+        if ($this->debug > 1) {
+            $this->debugMsg("+++GOT+++\n" . $data . "\n+++END+++");
+        }
+
+        $resp = $this->parseRequestHeaders($data, $reqCharset, $respCharset, $respEncoding);
+        if (!$resp) {
+            // this actually executes the request
+            $resp = $this->parseRequest($data, $reqCharset);
+
+            // save full body of request into response, for debugging purposes.
+            // NB: this is the _request_ data, not the response's own data, unlike what happens client-side
+            /// @todo try to move this injection to the resp. constructor or use a non-deprecated access method. Or, even
+            ///       better: just avoid setting this, and set debug info of the received http request in the request
+            ///       object instead? It's not like the developer misses access to _SERVER, _COOKIES though...
+            ///       Last but not least: the raw data might be of use to handler functions - but in decompressed form...
+            $resp->raw_data = $rawData;
+        }
+
+        if ($this->debug > 2 && static::$_xmlrpcs_occurred_errors != '') {
+            $this->debugMsg("+++PROCESSING ERRORS AND WARNINGS+++\n" .
+                static::$_xmlrpcs_occurred_errors . "+++END+++");
+        }
+
+        $header = $resp->xml_header($respCharset);
+        if ($this->debug > 0) {
+            $header .= $this->serializeDebug($respCharset);
+        }
+
+        // Do not create response serialization if it has already happened. Helps to build json magic
+        /// @todo what if the payload was created targeting a different charset than $respCharset?
+        ///       Also, if we do not call serialize(), the request will not set its content-type to have the charset declared
+        $payload = $resp->getPayload();
+        if (empty($payload)) {
+            $payload = $resp->serialize($respCharset);
+        }
+        $payload = $header . $payload;
+
+        if ($returnPayload) {
+            return $payload;
+        }
+
+        // if we get a warning/error that has output some text before here, then we cannot
+        // add a new header. We cannot say we are sending xml, either...
+        if (!headers_sent()) {
+            header('Content-Type: ' . $resp->getContentType());
+            // we do not know if client actually told us an accepted charset, but if it did we have to tell it what we did
+            header("Vary: Accept-Charset");
+
+            // http compression of output: only if we can do it, and we want to do it, and client asked us to,
+            // and php ini settings do not force it already
+            $phpNoSelfCompress = !ini_get('zlib.output_compression') && (ini_get('output_handler') != 'ob_gzhandler');
+            if ($this->compress_response && $respEncoding != '' && $phpNoSelfCompress) {
+                if (strpos($respEncoding, 'gzip') !== false && function_exists('gzencode')) {
+                    $payload = gzencode($payload);
+                    header("Content-Encoding: gzip");
+                    header("Vary: Accept-Encoding");
+                } elseif (strpos($respEncoding, 'deflate') !== false && function_exists('gzcompress')) {
+                    $payload = gzcompress($payload);
+                    header("Content-Encoding: deflate");
+                    header("Vary: Accept-Encoding");
+                }
+            }
+
+            // Do not output content-length header if php is compressing output for us: it will mess up measurements.
+            // Note that Apache/mod_php will add (and even alter!) the Content-Length header on its own, but only for
+            // responses up to 8000 bytes
+            if ($phpNoSelfCompress) {
+                header('Content-Length: ' . (int)strlen($payload));
+            }
+        } else {
+            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': http headers already sent before response is fully generated. Check for php warning or error messages');
+        }
+
+        print $payload;
+
+        // return response, in case subclasses want it
+        return $resp;
+    }
+
+    /**
+     * Add a method to the dispatch map.
+     *
+     * @param string $methodName the name with which the method will be made available
+     * @param callable $function the php function that will get invoked
+     * @param array[] $sig the array of valid method signatures.
+     *                     Each element is one signature: an array of strings with at least one element
+     *                     First element = type of returned value. Elements 2..N = types of parameters 1..N
+     * @param string $doc method documentation
+     * @param array[] $sigDoc the array of valid method signatures docs, following the format of $sig but with
+     *                        descriptions instead of types (one string for return type, one per param)
+     * @param string $parametersType to allow single method handlers to receive php values instead of a Request, or vice-versa
+     * @param int $exceptionHandling @see $this->exception_handling
+     * @return void
+     *
+     * @todo raise a warning if the user tries to register a 'system.' method
+     */
+    public function addToMap($methodName, $function, $sig = null, $doc = false, $sigDoc = false, $parametersType = false,
+        $exceptionHandling = false)
+    {
+       $this->add_to_map($methodName, $function, $sig, $doc, $sigDoc, $parametersType, $exceptionHandling);
+    }
+
+    /**
+     * Add a method to the dispatch map.
+     *
+     * @param string $methodName the name with which the method will be made available
+     * @param callable $function the php function that will get invoked
+     * @param array[] $sig the array of valid method signatures.
+     *                     Each element is one signature: an array of strings with at least one element
+     *                     First element = type of returned value. Elements 2..N = types of parameters 1..N
+     * @param string $doc method documentation
+     * @param array[] $sigDoc the array of valid method signatures docs, following the format of $sig but with
+     *                        descriptions instead of types (one string for return type, one per param)
+     * @param string $parametersType to allow single method handlers to receive php values instead of a Request, or vice-versa
+     * @param int $exceptionHandling @see $this->exception_handling
+     * @return void
+     *
+     * @todo raise a warning if the user tries to register a 'system.' method
+     * @deprecated use addToMap instead
+     */
+    public function add_to_map($methodName, $function, $sig = null, $doc = false, $sigDoc = false, $parametersType = false,
+        $exceptionHandling = false)
+    {
+        $this->logDeprecationUnlessCalledBy('addToMap');
+
+        $this->dmap[$methodName] = array(
+            'function' => $function,
+            'docstring' => $doc,
+        );
+        if ($sig) {
+            $this->dmap[$methodName]['signature'] = $sig;
+        }
+        if ($sigDoc) {
+            $this->dmap[$methodName]['signature_docs'] = $sigDoc;
+        }
+        if ($parametersType) {
+            $this->dmap[$methodName]['parameters_type'] = $parametersType;
+        }
+        if ($exceptionHandling !== false) {
+            $this->dmap[$methodName]['exception_handling'] = $exceptionHandling;
+        }
+    }
+
+    /**
+     * Verify type and number of parameters received against a list of known signatures.
+     *
+     * @param array|Request $in array of either xml-rpc value objects or xml-rpc type definitions
+     * @param array $sigs array of known signatures to match against
+     * @return array int, string
+     */
+    protected function verifySignature($in, $sigs)
+    {
+        // check each possible signature in turn
+        if (is_object($in)) {
+            $numParams = $in->getNumParams();
+        } else {
+            $numParams = count($in);
+        }
+        foreach ($sigs as $curSig) {
+            if (count($curSig) == $numParams + 1) {
+                $itsOK = 1;
+                for ($n = 0; $n < $numParams; $n++) {
+                    if (is_object($in)) {
+                        $p = $in->getParam($n);
+                        if ($p->kindOf() == 'scalar') {
+                            $pt = $p->scalarTyp();
+                        } else {
+                            $pt = $p->kindOf();
+                        }
+                    } else {
+                        $pt = ($in[$n] == 'i4') ? 'int' : strtolower($in[$n]); // dispatch maps never use i4...
+                    }
+
+                    // param index is $n+1, as first member of sig is return type
+                    if ($pt != $curSig[$n + 1] && $curSig[$n + 1] != Value::$xmlrpcValue) {
+                        $itsOK = 0;
+                        $pno = $n + 1;
+                        $wanted = $curSig[$n + 1];
+                        $got = $pt;
+                        break;
+                    }
+                }
+                if ($itsOK) {
+                    return array(1, '');
+                }
+            }
+        }
+        if (isset($wanted)) {
+            return array(0, "Wanted {$wanted}, got {$got} at param {$pno}");
+        } else {
+            return array(0, "No method signature matches number of parameters");
+        }
+    }
+
+    /**
+     * Parse http headers received along with xml-rpc request. If needed, inflate request.
+     *
+     * @return Response|null null on success or an error Response
+     */
+    protected function parseRequestHeaders(&$data, &$reqEncoding, &$respEncoding, &$respCompression)
+    {
+        // check if $_SERVER is populated: it might have been disabled via ini file
+        // (this is true even when in CLI mode)
+        if (count($_SERVER) == 0) {
+            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': cannot parse request headers as $_SERVER is not populated');
+        }
+
+        if ($this->debug > 1) {
+            if (function_exists('getallheaders')) {
+                $this->debugMsg(''); // empty line
+                foreach (getallheaders() as $name => $val) {
+                    $this->debugMsg("HEADER: $name: $val");
+                }
+            }
+        }
+
+        if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) {
+            $contentEncoding = str_replace('x-', '', $_SERVER['HTTP_CONTENT_ENCODING']);
+        } else {
+            $contentEncoding = '';
+        }
+
+        $rawData = $data;
+
+        // check if request body has been compressed and decompress it
+        if ($contentEncoding != '' && strlen($data)) {
+            if ($contentEncoding == 'deflate' || $contentEncoding == 'gzip') {
+                // if decoding works, use it. else assume data wasn't gzencoded
+                /// @todo test separately for gzinflate and gzuncompress
+                if (function_exists('gzinflate') && in_array($contentEncoding, $this->accepted_compression)) {
+                    if ($contentEncoding == 'deflate' && $degzdata = @gzuncompress($data)) {
+                        $data = $degzdata;
+                        if ($this->debug > 1) {
+                            $this->debugMsg("\n+++INFLATED REQUEST+++[" . strlen($data) . " chars]+++\n" . $data . "\n+++END+++");
+                        }
+                    } elseif ($contentEncoding == 'gzip' && $degzdata = @gzinflate(substr($data, 10))) {
+                        $data = $degzdata;
+                        if ($this->debug > 1) {
+                            $this->debugMsg("+++INFLATED REQUEST+++[" . strlen($data) . " chars]+++\n" . $data . "\n+++END+++");
+                        }
+                    } else {
+                        $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_decompress_fail'],
+                            PhpXmlRpc::$xmlrpcstr['server_decompress_fail'], '', array('raw_data' => $rawData)
+                        );
+
+                        return $r;
+                    }
+                } else {
+                    $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_cannot_decompress'],
+                        PhpXmlRpc::$xmlrpcstr['server_cannot_decompress'], '', array('raw_data' => $rawData)
+                    );
+
+                    return $r;
+                }
+            }
+        }
+
+        // check if client specified accepted charsets, and if we know how to fulfill the request
+        if ($this->response_charset_encoding == 'auto') {
+            $respEncoding = '';
+            if (isset($_SERVER['HTTP_ACCEPT_CHARSET'])) {
+                // here we check if we can match the client-requested encoding with the encodings we know we can generate.
+                // we parse q=0.x preferences instead of preferring the first charset specified
+                $http = new Http();
+                $clientAcceptedCharsets = $http->parseAcceptHeader($_SERVER['HTTP_ACCEPT_CHARSET']);
+                $knownCharsets = $this->getCharsetEncoder()->knownCharsets();
+                foreach ($clientAcceptedCharsets as $accepted) {
+                    foreach ($knownCharsets as $charset) {
+                        if (strtoupper($accepted) == strtoupper($charset)) {
+                            $respEncoding = $charset;
+                            break 2;
+                        }
+                    }
+                }
+            }
+        } else {
+            $respEncoding = $this->response_charset_encoding;
+        }
+
+        if (isset($_SERVER['HTTP_ACCEPT_ENCODING'])) {
+            $respCompression = $_SERVER['HTTP_ACCEPT_ENCODING'];
+        } else {
+            $respCompression = '';
+        }
+
+        // 'guestimate' request encoding
+        /// @todo check if mbstring is enabled and automagic input conversion is on: it might mingle with this check???
+        $parser = $this->getParser();
+        $reqEncoding = $parser->guessEncoding(isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : '',
+            $data);
+
+        return null;
+    }
+
+    /**
+     * Parse an xml chunk containing an xml-rpc request and execute the corresponding php function registered with the
+     * server.
+     * @internal this function will become protected in the future
+     *
+     * @param string $data the xml request
+     * @param string $reqEncoding (optional) the charset encoding of the xml request
+     * @return Response
+     * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
+     *
+     * @todo either rename this function or move the 'execute' part out of it...
+     */
+    public function parseRequest($data, $reqEncoding = '')
+    {
+        // decompose incoming XML into request structure
+
+        /// @todo move this block of code into the XMLParser
+        if ($reqEncoding != '') {
+            // Since parsing will fail if
+            // - charset is not specified in the xml declaration,
+            // - the encoding is not UTF8 and
+            // - there are non-ascii chars in the text,
+            // we try to work round that...
+            // The following code might be better for mb_string enabled installs, but it makes the lib about 200% slower...
+            //if (!is_valid_charset($reqEncoding, array('UTF-8')))
+            if (!in_array($reqEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) {
+                if (function_exists('mb_convert_encoding')) {
+                    $data = mb_convert_encoding($data, 'UTF-8', $reqEncoding);
+                } else {
+                    if ($reqEncoding == 'ISO-8859-1') {
+                        $data = utf8_encode($data);
+                    } else {
+                        $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': unsupported charset encoding of received request: ' . $reqEncoding);
+                    }
+                }
+            }
+        }
+        // PHP internally might use ISO-8859-1, so we have to tell the xml parser to give us back data in the expected charset.
+        // What if internal encoding is not in one of the 3 allowed? We use the broadest one, i.e. utf8
+        if (in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
+            $options = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
+        } else {
+            $options = array(XML_OPTION_TARGET_ENCODING => 'UTF-8', 'target_charset' => PhpXmlRpc::$xmlrpc_internalencoding);
+        }
+        // register a callback with the xml parser for when it finds the method name
+        $options['methodname_callback'] = array($this, 'methodNameCallback');
+
+        $xmlRpcParser = $this->getParser();
+        try {
+            $_xh = $xmlRpcParser->parse($data, $this->functions_parameters_type, XMLParser::ACCEPT_REQUEST, $options);
+            // BC
+            if (!is_array($_xh)) {
+                $_xh = $xmlRpcParser->_xh;
+            }
+        } catch (NoSuchMethodException $e) {
+            return new static::$responseClass(0, $e->getCode(), $e->getMessage());
+        }
+
+        if ($_xh['isf'] == 3) {
+            // (BC) we return XML error as a faultCode
+            preg_match('/^XML error ([0-9]+)/', $_xh['isf_reason'], $matches);
+            return new static::$responseClass(
+                0,
+                PhpXmlRpc::$xmlrpcerrxml + (int)$matches[1],
+                $_xh['isf_reason']);
+        } elseif ($_xh['isf']) {
+            /// @todo separate better the various cases, as we have done in Request::parseResponse: invalid xml-rpc vs.
+            ///       parsing error
+            return new static::$responseClass(
+                0,
+                PhpXmlRpc::$xmlrpcerr['invalid_request'],
+                PhpXmlRpc::$xmlrpcstr['invalid_request'] . ' ' . $_xh['isf_reason']);
+        } else {
+            // small layering violation in favor of speed and memory usage: we should allow the 'execute' method handle
+            // this, but in the most common scenario (xml-rpc values type server with some methods registered as phpvals)
+            // that would mean a useless encode+decode pass
+            if ($this->functions_parameters_type != 'xmlrpcvals' ||
+                (isset($this->dmap[$_xh['method']]['parameters_type']) &&
+                    ($this->dmap[$_xh['method']]['parameters_type'] != 'xmlrpcvals')
+                )
+            ) {
+                if ($this->debug > 1) {
+                    $this->debugMsg("\n+++PARSED+++\n" . var_export($_xh['params'], true) . "\n+++END+++");
+                }
+
+                return $this->execute($_xh['method'], $_xh['params'], $_xh['pt']);
+            } else {
+                // build a Request object with data parsed from xml and add parameters in
+                $req = new Request($_xh['method']);
+                /// @todo for more speed, we could just pass in the array to the constructor (and loose the type validation)...
+                for ($i = 0; $i < count($_xh['params']); $i++) {
+                    $req->addParam($_xh['params'][$i]);
+                }
+
+                if ($this->debug > 1) {
+                    $this->debugMsg("\n+++PARSED+++\n" . var_export($req, true) . "\n+++END+++");
+                }
+
+                return $this->execute($req);
+            }
+        }
+    }
+
+    /**
+     * Execute a method invoked by the client, checking parameters used.
+     *
+     * @param Request|string $req either a Request obj or a method name
+     * @param mixed[] $params array with method parameters as php types (only if $req is method name)
+     * @param string[] $paramTypes array with xml-rpc types of method parameters (only if $req is method name)
+     * @return Response
+     *
+     * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
+     */
+    protected function execute($req, $params = null, $paramTypes = null)
+    {
+        static::$_xmlrpcs_occurred_errors = '';
+        static::$_xmlrpc_debuginfo = '';
+
+        if (is_object($req)) {
+            $methodName = $req->method();
+        } else {
+            $methodName = $req;
+        }
+
+        $sysCall = $this->isSyscall($methodName);
+        $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
+
+        if (!isset($dmap[$methodName]['function'])) {
+            // No such method
+            return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['unknown_method'], PhpXmlRpc::$xmlrpcstr['unknown_method']);
+        }
+
+        // Check signature
+        if (isset($dmap[$methodName]['signature'])) {
+            $sig = $dmap[$methodName]['signature'];
+            if (is_object($req)) {
+                list($ok, $errStr) = $this->verifySignature($req, $sig);
+            } else {
+                list($ok, $errStr) = $this->verifySignature($paramTypes, $sig);
+            }
+            if (!$ok) {
+                // Didn't match.
+                return new static::$responseClass(
+                    0,
+                    PhpXmlRpc::$xmlrpcerr['incorrect_params'],
+                    PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": {$errStr}"
+                );
+            }
+        }
+
+        $func = $dmap[$methodName]['function'];
+
+        // let the 'class::function' syntax be accepted in dispatch maps
+        if (is_string($func) && strpos($func, '::')) {
+            $func = explode('::', $func);
+        }
+
+        // build string representation of function 'name'
+        if (is_array($func)) {
+            if (is_object($func[0])) {
+                $funcName = get_class($func[0]) . '->' . $func[1];
+            } else {
+                $funcName = implode('::', $func);
+            }
+        } else if ($func instanceof \Closure) {
+            $funcName = 'Closure';
+        } else {
+            $funcName = $func;
+        }
+
+        // verify that function to be invoked is in fact callable
+        if (!is_callable($func)) {
+            $this->getLogger()->error("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler is not callable");
+            return new static::$responseClass(
+                0,
+                PhpXmlRpc::$xmlrpcerr['server_error'],
+                PhpXmlRpc::$xmlrpcstr['server_error'] . ": no function matches method"
+            );
+        }
+
+        if (isset($dmap[$methodName]['exception_handling'])) {
+            $exception_handling = (int)$dmap[$methodName]['exception_handling'];
+        } else {
+            $exception_handling = $this->exception_handling;
+        }
+
+        // If debug level is 3, we should catch all errors generated during processing of user function, and log them
+        // as part of response
+        if ($this->debug > 2) {
+            self::$_xmlrpcs_prev_ehandler = set_error_handler(array('\PhpXmlRpc\Server', '_xmlrpcs_errorHandler'));
+        }
+
+        try {
+            // Allow mixed-convention servers
+            if (is_object($req)) {
+                // call an 'xml-rpc aware' function
+                if ($sysCall) {
+                    $r = call_user_func($func, $this, $req);
+                } else {
+                    $r = call_user_func($func, $req);
+                }
+                if (!is_a($r, 'PhpXmlRpc\Response')) {
+                    $this->getLogger()->error("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler does not return an xmlrpc response object but a " . gettype($r));
+                    if (is_a($r, 'PhpXmlRpc\Value')) {
+                        $r = new static::$responseClass($r);
+                    } else {
+                        $r = new static::$responseClass(
+                            0,
+                            PhpXmlRpc::$xmlrpcerr['server_error'],
+                            PhpXmlRpc::$xmlrpcstr['server_error'] . ": function does not return xmlrpc response object"
+                        );
+                    }
+                }
+            } else {
+                // call a 'plain php' function
+                if ($sysCall) {
+                    array_unshift($params, $this);
+                    $r = call_user_func_array($func, $params);
+                } else {
+                    // 3rd API convention for method-handling functions: EPI-style
+                    if ($this->functions_parameters_type == 'epivals') {
+                        $r = call_user_func_array($func, array($methodName, $params, $this->user_data));
+                        // mimic EPI behaviour: if we get an array that looks like an error, make it an error response
+                        if (is_array($r) && array_key_exists('faultCode', $r) && array_key_exists('faultString', $r)) {
+                            $r = new static::$responseClass(0, (integer)$r['faultCode'], (string)$r['faultString']);
+                        } else {
+                            // functions using EPI api should NOT return resp objects, so make sure we encode the
+                            // return type correctly
+                            $encoder = new Encoder();
+                            $r = new static::$responseClass($encoder->encode($r, array('extension_api')));
+                        }
+                    } else {
+                        $r = call_user_func_array($func, $params);
+                    }
+                }
+                // the return type can be either a Response object or a plain php value...
+                if (!is_a($r, '\PhpXmlRpc\Response')) {
+                    // q: what should we assume here about automatic encoding of datetimes and php classes instances?
+                    // a: let the user decide
+                    $encoder = new Encoder();
+                    $r = new static::$responseClass($encoder->encode($r, $this->phpvals_encoding_options));
+                }
+            }
+        /// @todo bump minimum php version to 7.1 and use a single catch clause instead of the duplicate blocks
+        } catch (\Exception $e) {
+            // (barring errors in the lib) an uncaught exception happened in the called function, we wrap it in a
+            // proper error-response
+            switch ($exception_handling) {
+                case 2:
+                    if ($this->debug > 2) {
+                        if (self::$_xmlrpcs_prev_ehandler) {
+                            set_error_handler(self::$_xmlrpcs_prev_ehandler);
+                        } else {
+                            restore_error_handler();
+                        }
+                    }
+                    throw $e;
+                case 1:
+                    $errCode = $e->getCode();
+                    if ($errCode == 0) {
+                        $errCode = PhpXmlRpc::$xmlrpcerr['server_error'];
+                    }
+                    $r = new static::$responseClass(0, $errCode, $e->getMessage());
+                    break;
+                default:
+                    $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']);
+            }
+        } catch (\Error $e) {
+            // (barring errors in the lib) an uncaught exception happened in the called function, we wrap it in a
+            // proper error-response
+            switch ($exception_handling) {
+                case 2:
+                    if ($this->debug > 2) {
+                        if (self::$_xmlrpcs_prev_ehandler) {
+                            set_error_handler(self::$_xmlrpcs_prev_ehandler);
+                        } else {
+                            restore_error_handler();
+                        }
+                    }
+                    throw $e;
+                case 1:
+                    $errCode = $e->getCode();
+                    if ($errCode == 0) {
+                        $errCode = PhpXmlRpc::$xmlrpcerr['server_error'];
+                    }
+                    $r = new static::$responseClass(0, $errCode, $e->getMessage());
+                    break;
+                default:
+                    $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']);
+            }
+        }
+
+        if ($this->debug > 2) {
+            // note: restore the error handler we found before calling the user func, even if it has been changed
+            // inside the func itself
+            if (self::$_xmlrpcs_prev_ehandler) {
+                set_error_handler(self::$_xmlrpcs_prev_ehandler);
+            } else {
+                restore_error_handler();
+            }
+        }
+
+        return $r;
+    }
+
+    /**
+     * Registered as callback for when the XMLParser has found the name of the method to execute.
+     * Handling that early allows to 1. stop parsing the rest of the xml if there is no such method registered, and
+     * 2. tweak the type of data that the parser will return, in case the server uses mixed-calling-convention
+     *
+     * @internal
+     * @param $methodName
+     * @param XMLParser $xmlParser
+     * @param resource $parser
+     * @return void
+     * @throws NoSuchMethodException
+     *
+     * @todo feature creep - we could validate here that the method in the dispatch map is valid, but that would mean
+     *       dirtying a lot the logic, as we would have back to both parseRequest() and execute() methods the info
+     *       about the matched method handler, in order to avoid doing the work twice...
+     */
+    public function methodNameCallback($methodName, $xmlParser, $parser)
+    {
+        $sysCall = $this->isSyscall($methodName);
+        $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
+
+        if (!isset($dmap[$methodName]['function'])) {
+            // No such method
+            throw new NoSuchMethodException(PhpXmlRpc::$xmlrpcstr['unknown_method'], PhpXmlRpc::$xmlrpcerr['unknown_method']);
+        }
+
+        // alter on-the-fly the config of the xml parser if needed
+        if (isset($dmap[$methodName]['parameters_type']) &&
+            $dmap[$methodName]['parameters_type'] != $this->functions_parameters_type) {
+            /// @todo this should be done by a method of the XMLParser
+            switch ($dmap[$methodName]['parameters_type']) {
+                case XMLParser::RETURN_PHP:
+                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast');
+                    break;
+                case XMLParser::RETURN_EPIVALS:
+                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_epi');
+                    break;
+                /// @todo log a warning on unsupported return type
+                case XMLParser::RETURN_XMLRPCVALS:
+                default:
+                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee');
+            }
+        }
+    }
+
+    /**
+     * Add a string to the 'internal debug message' (separate from 'user debug message').
+     *
+     * @param string $string
+     * @return void
+     */
+    protected function debugMsg($string)
+    {
+        $this->debug_info .= $string . "\n";
+    }
+
+    /**
+     * @param string $methName
+     * @return bool
+     */
+    protected function isSyscall($methName)
+    {
+        return (strpos($methName, "system.") === 0);
+    }
+
+    /**
+     * @param array $dmap
+     * @return $this
+     */
+    public function setDispatchMap($dmap)
+    {
+        $this->dmap = $dmap;
+        return $this;
+    }
+
+    /**
+     * @return array[]
+     */
+    public function getDispatchMap()
+    {
+        return $this->dmap;
+    }
+
+    /**
+     * @return array[]
+     */
+    public function getSystemDispatchMap()
+    {
+        if (!$this->allow_system_funcs) {
+            return array();
+        }
+
+        return array(
+            'system.listMethods' => array(
+                'function' => 'PhpXmlRpc\Server::_xmlrpcs_listMethods',
+                // listMethods: signature was either a string, or nothing.
+                // The useless string variant has been removed
+                'signature' => array(array(Value::$xmlrpcArray)),
+                'docstring' => 'This method lists all the methods that the XML-RPC server knows how to dispatch',
+                'signature_docs' => array(array('list of method names')),
+            ),
+            'system.methodHelp' => array(
+                'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodHelp',
+                'signature' => array(array(Value::$xmlrpcString, Value::$xmlrpcString)),
+                'docstring' => 'Returns help text if defined for the method passed, otherwise returns an empty string',
+                'signature_docs' => array(array('method description', 'name of the method to be described')),
+            ),
+            'system.methodSignature' => array(
+                'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodSignature',
+                'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcString)),
+                'docstring' => 'Returns an array of known signatures (an array of arrays) for the method name passed. If no signatures are known, returns a none-array (test for type != array to detect missing signature)',
+                'signature_docs' => array(array('list of known signatures, each sig being an array of xmlrpc type names', 'name of method to be described')),
+            ),
+            'system.multicall' => array(
+                'function' => 'PhpXmlRpc\Server::_xmlrpcs_multicall',
+                'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcArray)),
+                'docstring' => 'Boxcar multiple RPC calls in one request. See http://www.xmlrpc.com/discuss/msgReader$1208 for details',
+                'signature_docs' => array(array('list of response structs, where each struct has the usual members', 'list of calls, with each call being represented as a struct, with members "methodname" and "params"')),
+            ),
+            'system.getCapabilities' => array(
+                'function' => 'PhpXmlRpc\Server::_xmlrpcs_getCapabilities',
+                'signature' => array(array(Value::$xmlrpcStruct)),
+                'docstring' => 'This method lists all the capabilities that the XML-RPC server has: the (more or less standard) extensions to the xmlrpc spec that it adheres to',
+                'signature_docs' => array(array('list of capabilities, described as structs with a version number and url for the spec')),
+            ),
+        );
+    }
+
+    /**
+     * @return array[]
+     */
+    public function getCapabilities()
+    {
+        $outAr = array(
+            // xml-rpc spec: always supported
+            'xmlrpc' => array(
+                'specUrl' => 'http://www.xmlrpc.com/spec', // NB: the spec sits now at http://xmlrpc.com/spec.md
+                'specVersion' => 1
+            ),
+            // if we support system.xxx functions, we always support multicall, too...
+            'system.multicall' => array(
+                // Note that, as of 2006/09/17, the following URL does not respond anymore
+                'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208',
+                'specVersion' => 1
+            ),
+            // introspection: version 2! we support 'mixed', too.
+            // note: the php xml-rpc extension says this instead:
+            //   url http://xmlrpc-epi.sourceforge.net/specs/rfc.introspection.php, version 20010516
+            'introspection' => array(
+                'specUrl' => 'http://phpxmlrpc.sourceforge.net/doc-2/ch10.html',
+                'specVersion' => 2,
+            ),
+        );
+
+        // NIL extension
+        if (PhpXmlRpc::$xmlrpc_null_extension) {
+            $outAr['nil'] = array(
+                // Note that, as of 2023/01, the following URL does not respond anymore
+                'specUrl' => 'http://www.ontosys.com/xml-rpc/extensions.php',
+                'specVersion' => 1
+            );
+        }
+
+        // support for "standard" error codes
+        if (PhpXmlRpc::$xmlrpcerr['unknown_method'] === Interop::$xmlrpcerr['unknown_method']) {
+            $outAr['faults_interop'] = array(
+                'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
+                'specVersion' => 20010516
+            );
+        }
+
+        return $outAr;
+    }
+
+    /**
+     * @internal handler of a system. method
+     *
+     * @param Server $server
+     * @param Request $req
+     * @return Response
+     */
+    public static function _xmlrpcs_getCapabilities($server, $req = null)
+    {
+        $encoder = new Encoder();
+        return new static::$responseClass($encoder->encode($server->getCapabilities()));
+    }
+
+    /**
+     * @internal handler of a system. method
+     *
+     * @param Server $server
+     * @param Request $req if called in plain php values mode, second param is missing
+     * @return Response
+     */
+    public static function _xmlrpcs_listMethods($server, $req = null)
+    {
+        $outAr = array();
+        foreach ($server->dmap as $key => $val) {
+            $outAr[] = new Value($key, 'string');
+        }
+        foreach ($server->getSystemDispatchMap() as $key => $val) {
+            $outAr[] = new Value($key, 'string');
+        }
+
+        return new static::$responseClass(new Value($outAr, 'array'));
+    }
+
+    /**
+     * @internal handler of a system. method
+     *
+     * @param Server $server
+     * @param Request $req
+     * @return Response
+     */
+    public static function _xmlrpcs_methodSignature($server, $req)
+    {
+        // let's accept as parameter either an xml-rpc value or string
+        if (is_object($req)) {
+            $methName = $req->getParam(0);
+            $methName = $methName->scalarVal();
+        } else {
+            $methName = $req;
+        }
+        if ($server->isSyscall($methName)) {
+            $dmap = $server->getSystemDispatchMap();
+        } else {
+            $dmap = $server->dmap;
+        }
+        if (isset($dmap[$methName])) {
+            if (isset($dmap[$methName]['signature'])) {
+                $sigs = array();
+                foreach ($dmap[$methName]['signature'] as $inSig) {
+                    $curSig = array();
+                    foreach ($inSig as $sig) {
+                        $curSig[] = new Value($sig, 'string');
+                    }
+                    $sigs[] = new Value($curSig, 'array');
+                }
+                $r = new static::$responseClass(new Value($sigs, 'array'));
+            } else {
+                // NB: according to the official docs, we should be returning a
+                // "none-array" here, which means not-an-array
+                $r = new static::$responseClass(new Value('undef', 'string'));
+            }
+        } else {
+            $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
+        }
+
+        return $r;
+    }
+
+    /**
+     * @internal handler of a system. method
+     *
+     * @param Server $server
+     * @param Request $req
+     * @return Response
+     */
+    public static function _xmlrpcs_methodHelp($server, $req)
+    {
+        // let's accept as parameter either an xml-rpc value or string
+        if (is_object($req)) {
+            $methName = $req->getParam(0);
+            $methName = $methName->scalarVal();
+        } else {
+            $methName = $req;
+        }
+        if ($server->isSyscall($methName)) {
+            $dmap = $server->getSystemDispatchMap();
+        } else {
+            $dmap = $server->dmap;
+        }
+        if (isset($dmap[$methName])) {
+            if (isset($dmap[$methName]['docstring'])) {
+                $r = new static::$responseClass(new Value($dmap[$methName]['docstring'], 'string'));
+            } else {
+                $r = new static::$responseClass(new Value('', 'string'));
+            }
+        } else {
+            $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
+        }
+
+        return $r;
+    }
+
+    /**
+     * @internal this function will become protected in the future
+     *
+     * @param $err
+     * @return Value
+     */
+    public static function _xmlrpcs_multicall_error($err)
+    {
+        if (is_string($err)) {
+            $str = PhpXmlRpc::$xmlrpcstr["multicall_{$err}"];
+            $code = PhpXmlRpc::$xmlrpcerr["multicall_{$err}"];
+        } else {
+            $code = $err->faultCode();
+            $str = $err->faultString();
+        }
+        $struct = array();
+        $struct['faultCode'] = new Value($code, 'int');
+        $struct['faultString'] = new Value($str, 'string');
+
+        return new Value($struct, 'struct');
+    }
+
+    /**
+     * @internal this function will become protected in the future
+     *
+     * @param Server $server
+     * @param Value $call
+     * @return Value
+     */
+    public static function _xmlrpcs_multicall_do_call($server, $call)
+    {
+        if ($call->kindOf() != 'struct') {
+            return static::_xmlrpcs_multicall_error('notstruct');
+        }
+        $methName = @$call['methodName'];
+        if (!$methName) {
+            return static::_xmlrpcs_multicall_error('nomethod');
+        }
+        if ($methName->kindOf() != 'scalar' || $methName->scalarTyp() != 'string') {
+            return static::_xmlrpcs_multicall_error('notstring');
+        }
+        if ($methName->scalarVal() == 'system.multicall') {
+            return static::_xmlrpcs_multicall_error('recursion');
+        }
+
+        $params = @$call['params'];
+        if (!$params) {
+            return static::_xmlrpcs_multicall_error('noparams');
+        }
+        if ($params->kindOf() != 'array') {
+            return static::_xmlrpcs_multicall_error('notarray');
+        }
+
+        $req = new Request($methName->scalarVal());
+        foreach ($params as $i => $param) {
+            if (!$req->addParam($param)) {
+                $i++; // for error message, we count params from 1
+                return static::_xmlrpcs_multicall_error(new static::$responseClass(0,
+                    PhpXmlRpc::$xmlrpcerr['incorrect_params'],
+                    PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": probable xml error in param " . $i));
+            }
+        }
+
+        $result = $server->execute($req);
+
+        if ($result->faultCode() != 0) {
+            return static::_xmlrpcs_multicall_error($result); // Method returned fault.
+        }
+
+        return new Value(array($result->value()), 'array');
+    }
+
+    /**
+     * @internal this function will become protected in the future
+     *
+     * @param Server $server
+     * @param Value $call
+     * @return Value
+     */
+    public static function _xmlrpcs_multicall_do_call_phpvals($server, $call)
+    {
+        if (!is_array($call)) {
+            return static::_xmlrpcs_multicall_error('notstruct');
+        }
+        if (!array_key_exists('methodName', $call)) {
+            return static::_xmlrpcs_multicall_error('nomethod');
+        }
+        if (!is_string($call['methodName'])) {
+            return static::_xmlrpcs_multicall_error('notstring');
+        }
+        if ($call['methodName'] == 'system.multicall') {
+            return static::_xmlrpcs_multicall_error('recursion');
+        }
+        if (!array_key_exists('params', $call)) {
+            return static::_xmlrpcs_multicall_error('noparams');
+        }
+        if (!is_array($call['params'])) {
+            return static::_xmlrpcs_multicall_error('notarray');
+        }
+
+        // this is a simplistic hack, since we might have received
+        // base64 or datetime values, but they will be listed as strings here...
+        $pt = array();
+        $wrapper = new Wrapper();
+        foreach ($call['params'] as $val) {
+            // support EPI-encoded base64 and datetime values
+            if ($val instanceof \stdClass && isset($val->xmlrpc_type)) {
+                $pt[] = $val->xmlrpc_type == 'datetime' ? Value::$xmlrpcDateTime : $val->xmlrpc_type;
+            } else {
+                $pt[] = $wrapper->php2XmlrpcType(gettype($val));
+            }
+        }
+
+        $result = $server->execute($call['methodName'], $call['params'], $pt);
+
+        if ($result->faultCode() != 0) {
+            return static::_xmlrpcs_multicall_error($result); // Method returned fault.
+        }
+
+        return new Value(array($result->value()), 'array');
+    }
+
+    /**
+     * @internal handler of a system. method
+     *
+     * @param Server $server
+     * @param Request|array $req
+     * @return Response
+     */
+    public static function _xmlrpcs_multicall($server, $req)
+    {
+        $result = array();
+        // let's accept a plain list of php parameters, beside a single xml-rpc msg object
+        if (is_object($req)) {
+            $calls = $req->getParam(0);
+            foreach ($calls as $call) {
+                $result[] = static::_xmlrpcs_multicall_do_call($server, $call);
+            }
+        } else {
+            $numCalls = count($req);
+            for ($i = 0; $i < $numCalls; $i++) {
+                $result[$i] = static::_xmlrpcs_multicall_do_call_phpvals($server, $req[$i]);
+            }
+        }
+
+        return new static::$responseClass(new Value($result, 'array'));
+    }
+
+    /**
+     * Error handler used to track errors that occur during server-side execution of PHP code.
+     * This allows to report back to the client whether an internal error has occurred or not
+     * using an xml-rpc response object, instead of letting the client deal with the html junk
+     * that a PHP execution error on the server generally entails.
+     *
+     * NB: in fact a user defined error handler can only handle WARNING, NOTICE and USER_* errors.
+     *
+     * @internal
+     */
+    public static function _xmlrpcs_errorHandler($errCode, $errString, $filename = null, $lineNo = null, $context = null)
+    {
+        // obey the @ protocol
+        if (error_reporting() == 0) {
+            return;
+        }
+
+        //if ($errCode != E_NOTICE && $errCode != E_WARNING && $errCode != E_USER_NOTICE && $errCode != E_USER_WARNING)
+        if ($errCode != E_STRICT) {
+            static::error_occurred($errString);
+        }
+
+        // Try to avoid as much as possible disruption to the previous error handling mechanism in place
+        if (self::$_xmlrpcs_prev_ehandler == '') {
+            // The previous error handler was the default: all we should do is log error to the default error log
+            // (if level high enough)
+            if (ini_get('log_errors') && (intval(ini_get('error_reporting')) & $errCode)) {
+                // we can't use the functionality of LoggerAware, because this is a static method
+                if (self::$logger === null) {
+                    self::$logger = Logger::instance();
+                }
+                self::$logger->error($errString);
+            }
+        } else {
+            // Pass control on to previous error handler, trying to avoid loops...
+            if (self::$_xmlrpcs_prev_ehandler != array('\PhpXmlRpc\Server', '_xmlrpcs_errorHandler')) {
+                if (is_array(self::$_xmlrpcs_prev_ehandler)) {
+                    // the following works both with static class methods and plain object methods as error handler
+                    call_user_func_array(self::$_xmlrpcs_prev_ehandler, array($errCode, $errString, $filename, $lineNo, $context));
+                } else {
+                    $method = self::$_xmlrpcs_prev_ehandler;
+                    $method($errCode, $errString, $filename, $lineNo, $context);
+                }
+            }
+        }
+    }
+
+    // *** BC layer ***
+
+    /**
+     * @param string $charsetEncoding
+     * @return string
+     *
+     * @deprecated this method was moved to the Response class
+     */
+    protected function xml_header($charsetEncoding = '')
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        if ($charsetEncoding != '') {
+            return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\"?" . ">\n";
+        } else {
+            return "<?xml version=\"1.0\"?" . ">\n";
+        }
+    }
+
+    // we have to make this return by ref in order to allow calls such as `$resp->_cookies['name'] = ['value' => 'something'];`
+    public function &__get($name)
+    {
+        switch ($name) {
+            case self::OPT_ACCEPTED_COMPRESSION :
+            case self::OPT_ALLOW_SYSTEM_FUNCS:
+            case self::OPT_COMPRESS_RESPONSE:
+            case self::OPT_DEBUG:
+            case self::OPT_EXCEPTION_HANDLING:
+            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
+            case self::OPT_PHPVALS_ENCODING_OPTIONS:
+            case self::OPT_RESPONSE_CHARSET_ENCODING:
+                $this->logDeprecation('Getting property Request::' . $name . ' is deprecated');
+                return $this->$name;
+            case 'accepted_charset_encodings':
+                // manually implement the 'protected property' behaviour
+                $canAccess = false;
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+                if (isset($trace[1]) && isset($trace[1]['class'])) {
+                    if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) {
+                        $canAccess = true;
+                    }
+                }
+                if ($canAccess) {
+                    $this->logDeprecation('Getting property Request::' . $name . ' is deprecated');
+                    return $this->accepted_compression;
+                } else {
+                    trigger_error("Cannot access protected property Server::accepted_charset_encodings in " . __FILE__, E_USER_ERROR);
+                }
+                break;
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+                $result = null;
+                return $result;
+        }
+    }
+
+    public function __set($name, $value)
+    {
+        switch ($name) {
+            case self::OPT_ACCEPTED_COMPRESSION :
+            case self::OPT_ALLOW_SYSTEM_FUNCS:
+            case self::OPT_COMPRESS_RESPONSE:
+            case self::OPT_DEBUG:
+            case self::OPT_EXCEPTION_HANDLING:
+            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
+            case self::OPT_PHPVALS_ENCODING_OPTIONS:
+            case self::OPT_RESPONSE_CHARSET_ENCODING:
+                $this->logDeprecation('Setting property Request::' . $name . ' is deprecated');
+                $this->$name = $value;
+                break;
+            case 'accepted_charset_encodings':
+                // manually implement the 'protected property' behaviour
+                $canAccess = false;
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+                if (isset($trace[1]) && isset($trace[1]['class'])) {
+                    if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) {
+                        $canAccess = true;
+                    }
+                }
+                if ($canAccess) {
+                    $this->logDeprecation('Setting property Request::' . $name . ' is deprecated');
+                    $this->accepted_compression = $value;
+                } else {
+                    trigger_error("Cannot access protected property Server::accepted_charset_encodings in " . __FILE__, E_USER_ERROR);
+                }
+                break;
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __set(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+        }
+    }
+
+    public function __isset($name)
+    {
+        switch ($name) {
+            case self::OPT_ACCEPTED_COMPRESSION :
+            case self::OPT_ALLOW_SYSTEM_FUNCS:
+            case self::OPT_COMPRESS_RESPONSE:
+            case self::OPT_DEBUG:
+            case self::OPT_EXCEPTION_HANDLING:
+            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
+            case self::OPT_PHPVALS_ENCODING_OPTIONS:
+            case self::OPT_RESPONSE_CHARSET_ENCODING:
+                $this->logDeprecation('Checking property Request::' . $name . ' is deprecated');
+                return isset($this->$name);
+            case 'accepted_charset_encodings':
+                // manually implement the 'protected property' behaviour
+                $canAccess = false;
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+                if (isset($trace[1]) && isset($trace[1]['class'])) {
+                    if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) {
+                        $canAccess = true;
+                    }
+                }
+                if ($canAccess) {
+                    $this->logDeprecation('Checking property Request::' . $name . ' is deprecated');
+                    return isset($this->accepted_compression);
+                }
+                // break through voluntarily
+            default:
+                return false;
+        }
+    }
+
+    public function __unset($name)
+    {
+        switch ($name) {
+            case self::OPT_ACCEPTED_COMPRESSION :
+            case self::OPT_ALLOW_SYSTEM_FUNCS:
+            case self::OPT_COMPRESS_RESPONSE:
+            case self::OPT_DEBUG:
+            case self::OPT_EXCEPTION_HANDLING:
+            case self::OPT_FUNCTIONS_PARAMETERS_TYPE:
+            case self::OPT_PHPVALS_ENCODING_OPTIONS:
+            case self::OPT_RESPONSE_CHARSET_ENCODING:
+                $this->logDeprecation('Unsetting property Request::' . $name . ' is deprecated');
+                unset($this->$name);
+                break;
+            case 'accepted_charset_encodings':
+                // manually implement the 'protected property' behaviour
+                $canAccess = false;
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+                if (isset($trace[1]) && isset($trace[1]['class'])) {
+                    if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) {
+                        $canAccess = true;
+                    }
+                }
+                if ($canAccess) {
+                    $this->logDeprecation('Unsetting property Request::' . $name . ' is deprecated');
+                    unset($this->accepted_compression);
+                } else {
+                    trigger_error("Cannot access protected property Server::accepted_charset_encodings in " . __FILE__, E_USER_ERROR);
+                }
+                break;
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __unset(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+        }
+    }
+}

+ 27 - 0
includes/phpxmlrpc/Traits/CharsetEncoderAware.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace PhpXmlRpc\Traits;
+
+use PhpXmlRpc\Helper\Charset;
+
+trait CharsetEncoderAware
+{
+    protected static $charsetEncoder;
+
+    public function getCharsetEncoder()
+    {
+        if (self::$charsetEncoder === null) {
+            self::$charsetEncoder = Charset::instance();
+        }
+        return self::$charsetEncoder;
+    }
+
+    /**
+     * @param $charsetEncoder
+     * @return void
+     */
+    public static function setCharsetEncoder($charsetEncoder)
+    {
+        self::$charsetEncoder = $charsetEncoder;
+    }
+}

+ 40 - 0
includes/phpxmlrpc/Traits/DeprecationLogger.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace PhpXmlRpc\Traits;
+
+use PhpXmlRpc\PhpXmlRpc;
+
+trait DeprecationLogger
+{
+    use LoggerAware;
+
+    protected function logDeprecation($message)
+    {
+        if (PhpXmlRpc::$xmlrpc_silence_deprecations) {
+            return;
+        }
+
+        $this->getLogger()->warning('XML-RPC Deprecated: ' . $message);
+    }
+
+    /**
+     * @param string $callee
+     * @param string $expectedCaller atm only the method name is supported
+     * @return void
+     */
+    protected function logDeprecationUnlessCalledBy($expectedCaller)
+    {
+        if (PhpXmlRpc::$xmlrpc_silence_deprecations) {
+            return;
+        }
+
+        $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
+        /// @todo we should check as well $trace[2]['class'], and make sure that it is a descendent of the class passed in in $expectedCaller
+        if ($trace[2]['function'] === $expectedCaller) {
+            return;
+        }
+
+        $this->getLogger()->warning('XML-RPC Deprecated: ' . $trace[1]['class'] . '::' . $trace[1]['function'] .
+            ' is only supposed to be called by ' . $expectedCaller);
+    }
+}

+ 27 - 0
includes/phpxmlrpc/Traits/LoggerAware.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace PhpXmlRpc\Traits;
+
+use PhpXmlRpc\Helper\Logger;
+
+trait LoggerAware
+{
+    protected static $logger;
+
+    public function getLogger()
+    {
+        if (self::$logger === null) {
+            self::$logger = Logger::instance();
+        }
+        return self::$logger;
+    }
+
+    /**
+     * @param $logger
+     * @return void
+     */
+    public static function setLogger($logger)
+    {
+        self::$logger = $logger;
+    }
+}

+ 28 - 0
includes/phpxmlrpc/Traits/ParserAware.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace PhpXmlRpc\Traits;
+
+use PhpXmlRpc\Helper\XMLParser;
+
+trait ParserAware
+{
+    protected static $parser;
+
+    /// @todo feature-creep: allow passing in $options (but then, how to deal with changing options between invocations?)
+    public function getParser()
+    {
+        if (self::$parser === null) {
+            self::$parser = new XMLParser();
+        }
+        return self::$parser;
+    }
+
+    /**
+     * @param $parser
+     * @return void
+     */
+    public static function setParser($parser)
+    {
+        self::$parser = $parser;
+    }
+}

+ 45 - 0
includes/phpxmlrpc/Traits/PayloadBearer.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace PhpXmlRpc\Traits;
+
+trait PayloadBearer
+{
+    /** @var string */
+    protected $payload;
+    /** @var string */
+    protected $content_type = 'text/xml';
+
+    /**
+     * @internal
+     *
+     * @param string $payload
+     * @param string $contentType
+     * @return $this
+     */
+    public function setPayload($payload, $contentType = '')
+    {
+        $this->payload = $payload;
+
+        if ($contentType != '') {
+            $this->content_type = $contentType;
+        }
+
+        return $this;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPayload()
+    {
+        return $this->payload;
+    }
+
+    /**
+     * @return string
+     */
+    public function getContentType()
+    {
+        return $this->content_type;
+    }
+}

+ 741 - 0
includes/phpxmlrpc/Value.php

@@ -0,0 +1,741 @@
+<?php
+
+namespace PhpXmlRpc;
+
+use PhpXmlRpc\Exception\StateErrorException;
+use PhpXmlRpc\Exception\TypeErrorException;
+use PhpXmlRpc\Exception\ValueErrorException;
+use PhpXmlRpc\Traits\CharsetEncoderAware;
+use PhpXmlRpc\Traits\DeprecationLogger;
+
+/**
+ * This class enables the creation of values for XML-RPC, by encapsulating plain php values.
+ *
+ * @property Value[]|mixed $me deprecated - public access left in purely for BC. Access via scalarVal()/__construct()
+ * @property int $params $mytype - public access left in purely for BC. Access via kindOf()/__construct()
+ * @property string|null $_php_class deprecated - public access left in purely for BC.
+ */
+class Value implements \Countable, \IteratorAggregate, \ArrayAccess
+{
+    use CharsetEncoderAware;
+    use DeprecationLogger;
+
+    public static $xmlrpcI4 = "i4";
+    public static $xmlrpcI8 = "i8";
+    public static $xmlrpcInt = "int";
+    public static $xmlrpcBoolean = "boolean";
+    public static $xmlrpcDouble = "double";
+    public static $xmlrpcString = "string";
+    public static $xmlrpcDateTime = "dateTime.iso8601";
+    public static $xmlrpcBase64 = "base64";
+    public static $xmlrpcArray = "array";
+    public static $xmlrpcStruct = "struct";
+    public static $xmlrpcValue = "undefined";
+    public static $xmlrpcNull = "null";
+
+    public static $xmlrpcTypes = array(
+        "i4" => 1,
+        "i8" => 1,
+        "int" => 1,
+        "boolean" => 1,
+        "double" => 1,
+        "string" => 1,
+        "dateTime.iso8601" => 1,
+        "base64" => 1,
+        "array" => 2,
+        "struct" => 3,
+        "null" => 1,
+    );
+
+    /** @var Value[]|mixed */
+    protected $me = array();
+    /**
+     * @var int 0 for undef, 1 for scalar, 2 for array, 3 for struct
+     */
+    protected $mytype = 0;
+    /** @var string|null */
+    protected $_php_class = null;
+
+    /**
+     * Build an xml-rpc value.
+     *
+     * When no value or type is passed in, the value is left uninitialized, and the value can be added later.
+     *
+     * @param Value[]|mixed $val if passing in an array, all array elements should be PhpXmlRpc\Value themselves
+     * @param string $type any valid xml-rpc type name (lowercase): i4, int, boolean, string, double, dateTime.iso8601,
+     *                     base64, array, struct, null.
+     *                     If null, 'string' is assumed.
+     *                     You should refer to http://xmlrpc.com/spec.md for more information on what each of these mean.
+     */
+    public function __construct($val = -1, $type = '')
+    {
+        // optimization creep - do not call addXX, do it all inline.
+        // downside: booleans will not be coerced anymore
+        if ($val !== -1 || $type != '') {
+            switch ($type) {
+                case '':
+                    $this->mytype = 1;
+                    $this->me['string'] = $val;
+                    break;
+                case 'i4':
+                case 'i8':
+                case 'int':
+                case 'double':
+                case 'string':
+                case 'boolean':
+                case 'dateTime.iso8601':
+                case 'base64':
+                case 'null':
+                    $this->mytype = 1;
+                    $this->me[$type] = $val;
+                    break;
+                case 'array':
+                    $this->mytype = 2;
+                    $this->me['array'] = $val;
+                    break;
+                case 'struct':
+                    $this->mytype = 3;
+                    $this->me['struct'] = $val;
+                    break;
+                default:
+                    $this->getLogger()->error("XML-RPC: " . __METHOD__ . ": not a known type ($type)");
+            }
+        }
+    }
+
+    /**
+     * Add a single php value to an xml-rpc value.
+     *
+     * If the xml-rpc value is an array, the php value is added as its last element.
+     * If the xml-rpc value is empty (uninitialized), this method makes it a scalar value, and sets that value.
+     * Fails if the xml-rpc value is not an array (i.e. a struct or a scalar) and already initialized.
+     *
+     * @param mixed $val
+     * @param string $type allowed values: i4, i8, int, boolean, string, double, dateTime.iso8601, base64, null.
+     * @return int 1 or 0 on failure
+     *
+     * @todo arguably, as we have addArray to add elements to an Array value, and addStruct to add elements to a Struct
+     *       value, we should not allow this method to add values to an Array. The 'scalar' in the method name refers to
+     *       the expected state of the target object, not to the type of $val. Also, this works differently from
+     *       addScalar/addStruct in that, when adding an element to an array, it wraps it into a new Value
+     * @todo rename?
+     */
+    public function addScalar($val, $type = 'string')
+    {
+        $typeOf = null;
+        if (isset(static::$xmlrpcTypes[$type])) {
+            $typeOf = static::$xmlrpcTypes[$type];
+        }
+
+        if ($typeOf !== 1) {
+            $this->getLogger()->error("XML-RPC: " . __METHOD__ . ": not a scalar type ($type)");
+            return 0;
+        }
+
+        // coerce booleans into correct values
+        /// @todo we should either do it for datetimes, integers, i8 and doubles, too, or just plain remove this check,
+        ///       implemented on booleans only...
+        if ($type == static::$xmlrpcBoolean) {
+            if (strcasecmp($val, 'true') == 0 || $val == 1 || ($val == true && strcasecmp($val, 'false'))) {
+                $val = true;
+            } else {
+                $val = false;
+            }
+        }
+
+        switch ($this->mytype) {
+            case 1:
+                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': scalar xmlrpc value can have only one value');
+                return 0;
+            case 3:
+                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': cannot add anonymous scalar to struct xmlrpc value');
+                return 0;
+            case 2:
+                // we're adding a scalar value to an array here
+                /// @todo should we try avoiding re-wrapping Value objects?
+                $class = get_class($this);
+                $this->me['array'][] = new $class($val, $type);
+
+                return 1;
+            default:
+                // a scalar, so set the value and remember we're scalar
+                $this->me[$type] = $val;
+                $this->mytype = $typeOf;
+
+                return 1;
+        }
+    }
+
+    /**
+     * Add an array of xml-rpc value objects to an xml-rpc value.
+     *
+     * If the xml-rpc value is an array, the elements are appended to the existing ones.
+     * If the xml-rpc value is empty (uninitialized), this method makes it an array value, and sets that value.
+     * Fails otherwise.
+     *
+     * @param Value[] $values
+     * @return int 1 or 0 on failure
+     *
+     * @todo add some checking for $values to be an array of xml-rpc values?
+     * @todo rename to addToArray?
+     */
+    public function addArray($values)
+    {
+        if ($this->mytype == 0) {
+            $this->mytype = static::$xmlrpcTypes['array'];
+            $this->me['array'] = $values;
+
+            return 1;
+        } elseif ($this->mytype == 2) {
+            // we're adding to an array here
+            $this->me['array'] = array_merge($this->me['array'], $values);
+
+            return 1;
+        } else {
+            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': already initialized as a [' . $this->kindOf() . ']');
+            return 0;
+        }
+    }
+
+    /**
+     * Merges an array of named xml-rpc value objects into an xml-rpc value.
+     *
+     * If the xml-rpc value is a struct, the elements are merged with the existing ones (overwriting existing ones).
+     * If the xml-rpc value is empty (uninitialized), this method makes it a struct value, and sets that value.
+     * Fails otherwise.
+     *
+     * @param Value[] $values
+     * @return int 1 or 0 on failure
+     *
+     * @todo add some checking for $values to be an array of xml-rpc values?
+     * @todo rename to addToStruct?
+     */
+    public function addStruct($values)
+    {
+        if ($this->mytype == 0) {
+            $this->mytype = static::$xmlrpcTypes['struct'];
+            $this->me['struct'] = $values;
+
+            return 1;
+        } elseif ($this->mytype == 3) {
+            // we're adding to a struct here
+            $this->me['struct'] = array_merge($this->me['struct'], $values);
+
+            return 1;
+        } else {
+            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': already initialized as a [' . $this->kindOf() . ']');
+            return 0;
+        }
+    }
+
+    /**
+     * Returns a string describing the base type of the value.
+     *
+     * @return string either "struct", "array", "scalar" or "undef"
+     */
+    public function kindOf()
+    {
+        switch ($this->mytype) {
+            case 3:
+                return 'struct';
+            case 2:
+                return 'array';
+            case 1:
+                return 'scalar';
+            default:
+                return 'undef';
+        }
+    }
+
+
+    /**
+     * Returns the value of a scalar xml-rpc value (base 64 decoding is automatically handled here)
+     *
+     * @return mixed
+     */
+    public function scalarVal()
+    {
+        $b = reset($this->me);
+
+        return $b;
+    }
+
+    /**
+     * Returns the type of the xml-rpc value.
+     *
+     * @return string For integers, 'int' is always returned in place of 'i4'. 'i8' is considered a separate type and
+     *                returned as such
+     */
+    public function scalarTyp()
+    {
+        reset($this->me);
+        $a = key($this->me);
+        if ($a == static::$xmlrpcI4) {
+            $a = static::$xmlrpcInt;
+        }
+
+        return $a;
+    }
+
+    /**
+     * Returns the xml representation of the value. XML prologue not included.
+     *
+     * @param string $charsetEncoding the charset to be used for serialization. If null, US-ASCII is assumed
+     * @return string
+     */
+    public function serialize($charsetEncoding = '')
+    {
+        $val = reset($this->me);
+        $typ = key($this->me);
+
+        return '<value>' . $this->serializeData($typ, $val, $charsetEncoding) . "</value>\n";
+    }
+
+    /**
+     * @param string $typ
+     * @param Value[]|mixed $val
+     * @param string $charsetEncoding
+     * @return string
+     *
+     * @deprecated this should be folded back into serialize()
+     */
+    protected function serializeData($typ, $val, $charsetEncoding = '')
+    {
+        $this->logDeprecationUnlessCalledBy('serialize');
+
+        if (!isset(static::$xmlrpcTypes[$typ])) {
+            return '';
+        }
+
+        switch (static::$xmlrpcTypes[$typ]) {
+            case 1:
+                switch ($typ) {
+                    case static::$xmlrpcBase64:
+                        $rs = "<{$typ}>" . base64_encode($val) . "</{$typ}>";
+                        break;
+                    case static::$xmlrpcBoolean:
+                        $rs = "<{$typ}>" . ($val ? '1' : '0') . "</{$typ}>";
+                        break;
+                    case static::$xmlrpcString:
+                        // Do NOT use htmlentities, since it will produce named html entities, which are invalid xml
+                        $rs = "<{$typ}>" . $this->getCharsetEncoder()->encodeEntities($val, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "</{$typ}>";
+                        break;
+                    case static::$xmlrpcInt:
+                    case static::$xmlrpcI4:
+                    case static::$xmlrpcI8:
+                        $rs = "<{$typ}>" . (int)$val . "</{$typ}>";
+                        break;
+                    case static::$xmlrpcDouble:
+                        // avoid using standard conversion of float to string because it is locale-dependent,
+                        // and also because the xml-rpc spec forbids exponential notation.
+                        // sprintf('%F') could be most likely ok, but it fails e.g. on 2e-14.
+                        // The code below tries its best at keeping max precision while avoiding exp notation,
+                        // but there is of course no limit in the number of decimal places to be used...
+                        $rs = "<{$typ}>" . preg_replace('/\\.?0+$/', '', number_format((double)$val, PhpXmlRpc::$xmlpc_double_precision, '.', '')) . "</{$typ}>";
+                        break;
+                    case static::$xmlrpcDateTime:
+                        if (is_string($val)) {
+                            $rs = "<{$typ}>{$val}</{$typ}>";
+                        // DateTimeInterface is not present in php 5.4...
+                        } elseif (is_a($val, 'DateTimeInterface') || is_a($val, 'DateTime')) {
+                            $rs = "<{$typ}>" . $val->format('Ymd\TH:i:s') . "</{$typ}>";
+                        } elseif (is_int($val)) {
+                            $rs = "<{$typ}>" . date('Ymd\TH:i:s', $val) . "</{$typ}>";
+                        } else {
+                            // not really a good idea here: but what should we output anyway? left for backward compat...
+                            $rs = "<{$typ}>{$val}</{$typ}>";
+                        }
+                        break;
+                    case static::$xmlrpcNull:
+                        if (PhpXmlRpc::$xmlrpc_null_apache_encoding) {
+                            $rs = "<ex:nil/>";
+                        } else {
+                            $rs = "<nil/>";
+                        }
+                        break;
+                    default:
+                        // no standard type value should arrive here, but provide a possibility
+                        // for xml-rpc values of unknown type...
+                        $rs = "<{$typ}>{$val}</{$typ}>";
+                }
+                break;
+            case 3:
+                // struct
+                if ($this->_php_class) {
+                    $rs = '<struct php_class="' . $this->_php_class . "\">\n";
+                } else {
+                    $rs = "<struct>\n";
+                }
+                $charsetEncoder = $this->getCharsetEncoder();
+                /** @var Value $val2 */
+                foreach ($val as $key2 => $val2) {
+                    $rs .= '<member><name>' . $charsetEncoder->encodeEntities($key2, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "</name>\n";
+                    $rs .= $val2->serialize($charsetEncoding);
+                    $rs .= "</member>\n";
+                }
+                $rs .= '</struct>';
+                break;
+            case 2:
+                // array
+                $rs = "<array>\n<data>\n";
+                /** @var Value $element */
+                foreach ($val as $element) {
+                    $rs .= $element->serialize($charsetEncoding);
+                }
+                $rs .= "</data>\n</array>";
+                break;
+            default:
+                /// @todo log a warning?
+                $rs = '';
+                break;
+        }
+
+        return $rs;
+    }
+
+    /**
+     * Returns the number of members in an xml-rpc value:
+     * - 0 for uninitialized values
+     * - 1 for scalar values
+     * - the number of elements for struct and array values
+     *
+     * @return integer
+     */
+    #[\ReturnTypeWillChange]
+    public function count()
+    {
+        switch ($this->mytype) {
+            case 3:
+                return count($this->me['struct']);
+            case 2:
+                return count($this->me['array']);
+            case 1:
+                return 1;
+            default:
+                return 0;
+        }
+    }
+
+    /**
+     * Implements the IteratorAggregate interface
+     * @internal required to be public to implement an Interface
+     *
+     * @return \ArrayIterator
+     */
+    #[\ReturnTypeWillChange]
+    public function getIterator()
+    {
+        switch ($this->mytype) {
+            case 3:
+                return new \ArrayIterator($this->me['struct']);
+            case 2:
+                return new \ArrayIterator($this->me['array']);
+            case 1:
+                return new \ArrayIterator($this->me);
+            default:
+                return new \ArrayIterator();
+        }
+    }
+
+    /**
+     * @internal required to be public to implement an Interface
+     *
+     * @param mixed $offset
+     * @param mixed $value
+     * @return void
+     * @throws ValueErrorException|TypeErrorException
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetSet($offset, $value)
+    {
+        switch ($this->mytype) {
+            case 3:
+                if (!($value instanceof Value)) {
+                    throw new TypeErrorException('It is only possible to add Value objects to an XML-RPC Struct');
+                }
+                if (is_null($offset)) {
+                    // disallow struct members with empty names
+                    throw new ValueErrorException('It is not possible to add anonymous members to an XML-RPC Struct');
+                } else {
+                    $this->me['struct'][$offset] = $value;
+                }
+                return;
+            case 2:
+                if (!($value instanceof Value)) {
+                    throw new TypeErrorException('It is only possible to add Value objects to an XML-RPC Array');
+                }
+                if (is_null($offset)) {
+                    $this->me['array'][] = $value;
+                } else {
+                    // nb: we are not checking that $offset is above the existing array range...
+                    $this->me['array'][$offset] = $value;
+                }
+                return;
+            case 1:
+                /// @todo: should we handle usage of i4 to retrieve int (in both set/unset/isset)? After all we consider
+                ///        'int' to be the preferred form, as evidenced in scalarTyp()
+                reset($this->me);
+                $type = key($this->me);
+                if ($type != $offset && ($type != 'i4' || $offset != 'int')) {
+                    throw new ValueErrorException('...');
+                }
+                $this->me[$type] = $value;
+                return;
+            default:
+                // it would be nice to allow empty values to be turned into non-empty ones this way, but we miss info to do so
+                throw new ValueErrorException("XML-RPC Value is of type 'undef' and its value can not be set using array index");
+        }
+    }
+
+    /**
+     * @internal required to be public to implement an Interface
+     *
+     * @param mixed $offset
+     * @return bool
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetExists($offset)
+    {
+        switch ($this->mytype) {
+            case 3:
+                return isset($this->me['struct'][$offset]);
+            case 2:
+                return isset($this->me['array'][$offset]);
+            case 1:
+                // handle i4 vs int
+                if ($offset == 'i4') {
+                    // to be consistent with set and unset, we disallow usage of i4 to check for int
+                    reset($this->me);
+                    return $offset == key($this->me);
+                } else {
+                    return $offset == $this->scalarTyp();
+                }
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * @internal required to be public to implement an Interface
+     *
+     * @param mixed $offset
+     * @return void
+     * @throws ValueErrorException|StateErrorException
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetUnset($offset)
+    {
+        switch ($this->mytype) {
+            case 3:
+                unset($this->me['struct'][$offset]);
+                return;
+            case 2:
+                unset($this->me['array'][$offset]);
+                return;
+            case 1:
+                // can not remove value from a scalar
+                /// @todo feature creep - allow this to move back the value to 'undef' state?
+                throw new StateErrorException("XML-RPC Value is of type 'scalar' and its value can not be unset using array index");
+            default:
+                throw new StateErrorException("XML-RPC Value is of type 'undef' and its value can not be unset using array index");
+        }
+    }
+
+    /**
+     * @internal required to be public to implement an Interface
+     *
+     * @param mixed $offset
+     * @return mixed|Value|null
+     * @throws StateErrorException
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet($offset)
+    {
+        switch ($this->mytype) {
+            case 3:
+                return isset($this->me['struct'][$offset]) ? $this->me['struct'][$offset] : null;
+            case 2:
+                return isset($this->me['array'][$offset]) ? $this->me['array'][$offset] : null;
+            case 1:
+                /// @todo what to return on bad type: null or exception?
+                $value = reset($this->me);
+                $type = key($this->me);
+                return $type == $offset ? $value : (($type == 'i4' && $offset == 'int') ? $value : null);
+            default:
+                // return null or exception?
+                throw new StateErrorException("XML-RPC Value is of type 'undef' and can not be accessed using array index");
+        }
+    }
+
+    // *** BC layer ***
+
+    /**
+     * Checks whether a struct member with a given name is present.
+     *
+     * Works only on xml-rpc values of type struct.
+     *
+     * @param string $key the name of the struct member to be looked up
+     * @return boolean
+     *
+     * @deprecated use array access, e.g. isset($val[$key])
+     */
+    public function structMemExists($key)
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        return array_key_exists($key, $this->me['struct']);
+    }
+
+    /**
+     * Returns the value of a given struct member (an xml-rpc value object in itself).
+     * Will raise a php warning if struct member of given name does not exist.
+     *
+     * @param string $key the name of the struct member to be looked up
+     * @return Value
+     *
+     * @deprecated use array access, e.g. $val[$key]
+     */
+    public function structMem($key)
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        return $this->me['struct'][$key];
+    }
+
+    /**
+     * Reset internal pointer for xml-rpc values of type struct.
+     * @return void
+     *
+     * @deprecated iterate directly over the object using foreach instead
+     */
+    public function structReset()
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        reset($this->me['struct']);
+    }
+
+    /**
+     * Return next member element for xml-rpc values of type struct.
+     *
+     * @return array having the same format as PHP's `each` method
+     *
+     * @deprecated iterate directly over the object using foreach instead
+     */
+    public function structEach()
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        $key = key($this->me['struct']);
+        $value = current($this->me['struct']);
+        next($this->me['struct']);
+        return array(1 => $value, 'value' => $value, 0 => $key, 'key' => $key);
+    }
+
+    /**
+     * Returns the n-th member of an xml-rpc value of array type.
+     *
+     * @param integer $key the index of the value to be retrieved (zero based)
+     *
+     * @return Value
+     *
+     * @deprecated use array access, e.g. $val[$key]
+     */
+    public function arrayMem($key)
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        return $this->me['array'][$key];
+    }
+
+    /**
+     * Returns the number of members in an xml-rpc value of array type.
+     *
+     * @return integer
+     *
+     * @deprecated use count() instead
+     */
+    public function arraySize()
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        return count($this->me['array']);
+    }
+
+    /**
+     * Returns the number of members in an xml-rpc value of struct type.
+     *
+     * @return integer
+     *
+     * @deprecated use count() instead
+     */
+    public function structSize()
+    {
+        $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated');
+
+        return count($this->me['struct']);
+    }
+
+    // we have to make this return by ref in order to allow calls such as `$resp->_cookies['name'] = ['value' => 'something'];`
+    public function &__get($name)
+    {
+        switch ($name) {
+            case 'me':
+            case 'mytype':
+            case '_php_class':
+                $this->logDeprecation('Getting property Value::' . $name . ' is deprecated');
+                return $this->$name;
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+                $result = null;
+                return $result;
+        }
+    }
+
+    public function __set($name, $value)
+    {
+        switch ($name) {
+            case 'me':
+            case 'mytype':
+            case '_php_class':
+                $this->logDeprecation('Setting property Value::' . $name . ' is deprecated');
+                $this->$name = $value;
+                break;
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __set(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+        }
+    }
+
+    public function __isset($name)
+    {
+        switch ($name) {
+            case 'me':
+            case 'mytype':
+            case '_php_class':
+                $this->logDeprecation('Checking property Value::' . $name . ' is deprecated');
+                return isset($this->$name);
+            default:
+                return false;
+        }
+    }
+
+    public function __unset($name)
+    {
+        switch ($name) {
+            case 'me':
+            case 'mytype':
+            case '_php_class':
+                $this->logDeprecation('Unsetting property Value::' . $name . ' is deprecated');
+                unset($this->$name);
+                break;
+            default:
+                /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
+                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
+                trigger_error('Undefined property via __unset(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
+        }
+    }
+}

+ 1258 - 0
includes/phpxmlrpc/Wrapper.php

@@ -0,0 +1,1258 @@
+<?php
+/**
+ * @author Gaetano Giunta
+ * @copyright (C) 2006-2023 G. Giunta
+ * @license code licensed under the BSD License: see file license.txt
+ */
+
+namespace PhpXmlRpc;
+
+use PhpXmlRpc\Exception\ValueErrorException;
+use PhpXmlRpc\Traits\LoggerAware;
+
+/**
+ * PHPXMLRPC "wrapper" class - generate stubs to transparently access xml-rpc methods as php functions and vice-versa.
+ * Note: this class implements the PROXY pattern, but it is not named so to avoid confusion with http proxies.
+ *
+ * @todo use some better templating system for code generation?
+ * @todo implement method wrapping with preservation of php objs in calls
+ * @todo add support for 'epivals' mode
+ * @todo allow setting custom namespace for generated wrapping code
+ */
+class Wrapper
+{
+    use LoggerAware;
+
+    /**
+     * @var object[]
+     * Used to hold a reference to object instances whose methods get wrapped by wrapPhpFunction(), in 'create source' mode
+     * @internal this property will become protected in the future
+     */
+    public static $objHolder = array();
+
+    /** @var string */
+    protected static $namespace = '\\PhpXmlRpc\\';
+
+    /**
+     * Given a string defining a php type or phpxmlrpc type (loosely defined: strings
+     * accepted come from javadoc blocks), return corresponding phpxmlrpc type.
+     * Notes:
+     * - for php 'resource' types returns empty string, since resources cannot be serialized;
+     * - for php class names returns 'struct', since php objects can be serialized as xml-rpc structs
+     * - for php arrays always return array, even though arrays sometimes serialize as structs...
+     * - for 'void' and 'null' returns 'undefined'
+     *
+     * @param string $phpType
+     * @return string
+     *
+     * @todo support notation `something[]` as 'array'
+     * @todo check if nil support is enabled when finding null
+     */
+    public function php2XmlrpcType($phpType)
+    {
+        switch (strtolower($phpType)) {
+            case 'string':
+                return Value::$xmlrpcString;
+            case 'integer':
+            case Value::$xmlrpcInt: // 'int'
+            case Value::$xmlrpcI4:
+            case Value::$xmlrpcI8:
+                return Value::$xmlrpcInt;
+            case Value::$xmlrpcDouble: // 'double'
+                return Value::$xmlrpcDouble;
+            case 'bool':
+            case Value::$xmlrpcBoolean: // 'boolean'
+            case 'false':
+            case 'true':
+                return Value::$xmlrpcBoolean;
+            case Value::$xmlrpcArray: // 'array':
+            case 'array[]';
+                return Value::$xmlrpcArray;
+            case 'object':
+            case Value::$xmlrpcStruct: // 'struct'
+                return Value::$xmlrpcStruct;
+            case Value::$xmlrpcBase64:
+                return Value::$xmlrpcBase64;
+            case 'resource':
+                return '';
+            default:
+                if (class_exists($phpType)) {
+                    // DateTimeInterface is not present in php 5.4...
+                    if (is_a($phpType, 'DateTimeInterface') || is_a($phpType, 'DateTime')) {
+                        return Value::$xmlrpcDateTime;
+                    }
+                    return Value::$xmlrpcStruct;
+                } else {
+                    // unknown: might be any 'extended' xml-rpc type
+                    return Value::$xmlrpcValue;
+                }
+        }
+    }
+
+    /**
+     * Given a string defining a phpxmlrpc type return the corresponding php type.
+     *
+     * @param string $xmlrpcType
+     * @return string
+     */
+    public function xmlrpc2PhpType($xmlrpcType)
+    {
+        switch (strtolower($xmlrpcType)) {
+            case 'base64':
+            case 'datetime.iso8601':
+            case 'string':
+                return Value::$xmlrpcString;
+            case 'int':
+            case 'i4':
+            case 'i8':
+                return 'integer';
+            case 'struct':
+            case 'array':
+                return 'array';
+            case 'double':
+                return 'float';
+            case 'undefined':
+                return 'mixed';
+            case 'boolean':
+            case 'null':
+            default:
+                // unknown: might be any xml-rpc type
+                return strtolower($xmlrpcType);
+        }
+    }
+
+    /**
+     * Given a user-defined PHP function, create a PHP 'wrapper' function that can be exposed as xml-rpc method from an
+     * xml-rpc server object and called from remote clients (as well as its corresponding signature info).
+     *
+     * Since php is a typeless language, to infer types of input and output parameters, it relies on parsing the
+     * javadoc-style comment block associated with the given function. Usage of xml-rpc native types (such as
+     * datetime.dateTime.iso8601 and base64) in the '@param' tag is also allowed, if you need the php function to
+     * receive/send data in that particular format (note that base64 encoding/decoding is transparently carried out by
+     * the lib, while datetime values are passed around as strings)
+     *
+     * Known limitations:
+     * - only works for user-defined functions, not for PHP internal functions (reflection does not support retrieving
+     *   number/type of params for those)
+     * - functions returning php objects will generate special structs in xml-rpc responses: when the xml-rpc decoding of
+     *   those responses is carried out by this same lib, using the appropriate param in php_xmlrpc_decode, the php
+     *   objects will be rebuilt.
+     *   In short: php objects can be serialized, too (except for their resource members), using this function.
+     *   Other libs might choke on the very same xml that will be generated in this case (i.e. it has a nonstandard
+     *   attribute on struct element tags)
+     *
+     * Note that since rel. 2.0RC3 the preferred method to have the server call 'standard' php functions (i.e. functions
+     * not expecting a single Request obj as parameter) is by making use of the $functions_parameters_type and
+     * $exception_handling properties.
+     *
+     * @param \Callable $callable the PHP user function to be exposed as xml-rpc method: a closure, function name, array($obj, 'methodname') or array('class', 'methodname') are ok
+     * @param string $newFuncName (optional) name for function to be created. Used only when return_source in $extraOptions is true
+     * @param array $extraOptions (optional) array of options for conversion. valid values include:
+     *                            - bool return_source     when true, php code w. function definition will be returned, instead of a closure
+     *                            - bool encode_nulls      let php objects be sent to server using <nil> elements instead of empty strings
+     *                            - bool encode_php_objs   let php objects be sent to server using the 'improved' xml-rpc notation, so server can deserialize them as php objects
+     *                            - bool decode_php_objs   --- WARNING !!! possible security hazard. only use it with trusted servers ---
+     *                            - bool suppress_warnings remove from produced xml any warnings generated at runtime by the php function being invoked
+     * @return array|false false on error, or an array containing the name of the new php function,
+     *                     its signature and docs, to be used in the server dispatch map
+     *
+     * @todo decide how to deal with params passed by ref in function definition: bomb out or allow?
+     * @todo finish using phpdoc info to build method sig if all params are named but out of order
+     * @todo add a check for params of 'resource' type
+     * @todo add some error logging when returning false?
+     * @todo what to do when the PHP function returns NULL? We are currently returning an empty string value...
+     * @todo add an option to suppress php warnings in invocation of user function, similar to server debug level 3?
+     * @todo add a verbatim_object_copy parameter to allow avoiding usage the same obj instance?
+     * @todo add an option to allow generated function to skip validation of number of parameters, as that is done by the server anyway
+     */
+    public function wrapPhpFunction($callable, $newFuncName = '', $extraOptions = array())
+    {
+        $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true;
+
+        if (is_string($callable) && strpos($callable, '::') !== false) {
+            $callable = explode('::', $callable);
+        }
+        if (is_array($callable)) {
+            if (count($callable) < 2 || (!is_string($callable[0]) && !is_object($callable[0]))) {
+                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': syntax for function to be wrapped is wrong');
+                return false;
+            }
+            if (is_string($callable[0])) {
+                $plainFuncName = implode('::', $callable);
+            } elseif (is_object($callable[0])) {
+                $plainFuncName = get_class($callable[0]) . '->' . $callable[1];
+            }
+            $exists = method_exists($callable[0], $callable[1]);
+        } else if ($callable instanceof \Closure) {
+            // we do not support creating code which wraps closures, as php does not allow to serialize them
+            if (!$buildIt) {
+                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': a closure can not be wrapped in generated source code');
+                return false;
+            }
+
+            $plainFuncName = 'Closure';
+            $exists = true;
+        } else {
+            $plainFuncName = $callable;
+            $exists = function_exists($callable);
+        }
+
+        if (!$exists) {
+            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': function to be wrapped is not defined: ' . $plainFuncName);
+            return false;
+        }
+
+        $funcDesc = $this->introspectFunction($callable, $plainFuncName);
+        if (!$funcDesc) {
+            return false;
+        }
+
+        $funcSigs = $this->buildMethodSignatures($funcDesc);
+
+        if ($buildIt) {
+            $callable = $this->buildWrapFunctionClosure($callable, $extraOptions, $plainFuncName, $funcDesc);
+        } else {
+            $newFuncName = $this->newFunctionName($callable, $newFuncName, $extraOptions);
+            $code = $this->buildWrapFunctionSource($callable, $newFuncName, $extraOptions, $plainFuncName, $funcDesc);
+        }
+
+        $ret = array(
+            'function' => $callable,
+            'signature' => $funcSigs['sigs'],
+            'docstring' => $funcDesc['desc'],
+            'signature_docs' => $funcSigs['sigsDocs'],
+        );
+        if (!$buildIt) {
+            $ret['function'] = $newFuncName;
+            $ret['source'] = $code;
+        }
+        return $ret;
+    }
+
+    /**
+     * Introspect a php callable and its phpdoc block and extract information about its signature
+     *
+     * @param callable $callable
+     * @param string $plainFuncName
+     * @return array|false
+     */
+    protected function introspectFunction($callable, $plainFuncName)
+    {
+        // start to introspect PHP code
+        if (is_array($callable)) {
+            $func = new \ReflectionMethod($callable[0], $callable[1]);
+            if ($func->isPrivate()) {
+                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is private: ' . $plainFuncName);
+                return false;
+            }
+            if ($func->isProtected()) {
+                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is protected: ' . $plainFuncName);
+                return false;
+            }
+            if ($func->isConstructor()) {
+                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is the constructor: ' . $plainFuncName);
+                return false;
+            }
+            if ($func->isDestructor()) {
+                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is the destructor: ' . $plainFuncName);
+                return false;
+            }
+            if ($func->isAbstract()) {
+                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': method to be wrapped is abstract: ' . $plainFuncName);
+                return false;
+            }
+            /// @todo add more checks for static vs. nonstatic?
+        } else {
+            $func = new \ReflectionFunction($callable);
+        }
+        if ($func->isInternal()) {
+            /// @todo from PHP 5.1.0 onward, we should be able to use invokeargs instead of getparameters to fully
+            ///       reflect internal php functions
+            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': function to be wrapped is internal: ' . $plainFuncName);
+            return false;
+        }
+
+        // retrieve parameter names, types and description from javadoc comments
+
+        // function description
+        $desc = '';
+        // type of return val: by default 'any'
+        $returns = Value::$xmlrpcValue;
+        // desc of return val
+        $returnsDocs = '';
+        // type + name of function parameters
+        $paramDocs = array();
+
+        $docs = $func->getDocComment();
+        if ($docs != '') {
+            $docs = explode("\n", $docs);
+            $i = 0;
+            foreach ($docs as $doc) {
+                $doc = trim($doc, " \r\t/*");
+                if (strlen($doc) && strpos($doc, '@') !== 0 && !$i) {
+                    if ($desc) {
+                        $desc .= "\n";
+                    }
+                    $desc .= $doc;
+                } elseif (strpos($doc, '@param') === 0) {
+                    // syntax: @param type $name [desc]
+                    if (preg_match('/@param\s+(\S+)\s+(\$\S+)\s*(.+)?/', $doc, $matches)) {
+                        $name = strtolower(trim($matches[2]));
+                        //$paramDocs[$name]['name'] = trim($matches[2]);
+                        $paramDocs[$name]['doc'] = isset($matches[3]) ? $matches[3] : '';
+                        $paramDocs[$name]['type'] = $matches[1];
+                    }
+                    $i++;
+                } elseif (strpos($doc, '@return') === 0) {
+                    // syntax: @return type [desc]
+                    if (preg_match('/@return\s+(\S+)(\s+.+)?/', $doc, $matches)) {
+                        $returns = $matches[1];
+                        if (isset($matches[2])) {
+                            $returnsDocs = trim($matches[2]);
+                        }
+                    }
+                }
+            }
+        }
+
+        // execute introspection of actual function prototype
+        $params = array();
+        $i = 0;
+        foreach ($func->getParameters() as $paramObj) {
+            $params[$i] = array();
+            $params[$i]['name'] = '$' . $paramObj->getName();
+            $params[$i]['isoptional'] = $paramObj->isOptional();
+            $i++;
+        }
+
+        return array(
+            'desc' => $desc,
+            'docs' => $docs,
+            'params' => $params, // array, positionally indexed
+            'paramDocs' => $paramDocs, // array, indexed by name
+            'returns' => $returns,
+            'returnsDocs' =>$returnsDocs,
+        );
+    }
+
+    /**
+     * Given the method description given by introspection, create method signature data
+     *
+     * @param array $funcDesc as generated by self::introspectFunction()
+     * @return array
+     *
+     * @todo support better docs with multiple types separated by pipes by creating multiple signatures
+     *       (this is questionable, as it might produce a big matrix of possible signatures with many such occurrences)
+     */
+    protected function buildMethodSignatures($funcDesc)
+    {
+        $i = 0;
+        $parsVariations = array();
+        $pars = array();
+        $pNum = count($funcDesc['params']);
+        foreach ($funcDesc['params'] as $param) {
+            /* // match by name real param and documented params
+            $name = strtolower($param['name']);
+            if (!isset($funcDesc['paramDocs'][$name])) {
+                $funcDesc['paramDocs'][$name] = array();
+            }
+            if (!isset($funcDesc['paramDocs'][$name]['type'])) {
+                $funcDesc['paramDocs'][$name]['type'] = 'mixed';
+            }*/
+
+            if ($param['isoptional']) {
+                // this particular parameter is optional. save as valid previous list of parameters
+                $parsVariations[] = $pars;
+            }
+
+            $pars[] = "\$p$i";
+            $i++;
+            if ($i == $pNum) {
+                // last allowed parameters combination
+                $parsVariations[] = $pars;
+            }
+        }
+
+        if (count($parsVariations) == 0) {
+            // only known good synopsis = no parameters
+            $parsVariations[] = array();
+        }
+
+        $sigs = array();
+        $sigsDocs = array();
+        foreach ($parsVariations as $pars) {
+            // build a signature
+            $sig = array($this->php2XmlrpcType($funcDesc['returns']));
+            $pSig = array($funcDesc['returnsDocs']);
+            for ($i = 0; $i < count($pars); $i++) {
+                $name = strtolower($funcDesc['params'][$i]['name']);
+                if (isset($funcDesc['paramDocs'][$name]['type'])) {
+                    $sig[] = $this->php2XmlrpcType($funcDesc['paramDocs'][$name]['type']);
+                } else {
+                    $sig[] = Value::$xmlrpcValue;
+                }
+                $pSig[] = isset($funcDesc['paramDocs'][$name]['doc']) ? $funcDesc['paramDocs'][$name]['doc'] : '';
+            }
+            $sigs[] = $sig;
+            $sigsDocs[] = $pSig;
+        }
+
+        return array(
+            'sigs' => $sigs,
+            'sigsDocs' => $sigsDocs
+        );
+    }
+
+    /**
+     * Creates a closure that will execute $callable
+     *
+     * @param $callable
+     * @param array $extraOptions
+     * @param string $plainFuncName
+     * @param array $funcDesc
+     * @return \Closure
+     *
+     * @todo validate params? In theory all validation is left to the dispatch map...
+     * @todo add support for $catchWarnings
+     */
+    protected function buildWrapFunctionClosure($callable, $extraOptions, $plainFuncName, $funcDesc)
+    {
+        /**
+         * @param Request $req
+         *
+         * @return mixed
+         */
+        $function = function($req) use($callable, $extraOptions, $funcDesc)
+        {
+            $encoderClass = static::$namespace.'Encoder';
+            $responseClass = static::$namespace.'Response';
+            $valueClass = static::$namespace.'Value';
+
+            // validate number of parameters received
+            // this should be optional really, as we assume the server does the validation
+            $minPars = count($funcDesc['params']);
+            $maxPars = $minPars;
+            foreach ($funcDesc['params'] as $i => $param) {
+                if ($param['isoptional']) {
+                    // this particular parameter is optional. We assume later ones are as well
+                    $minPars = $i;
+                    break;
+                }
+            }
+            $numPars = $req->getNumParams();
+            if ($numPars < $minPars || $numPars > $maxPars) {
+                return new $responseClass(0, 3, 'Incorrect parameters passed to method');
+            }
+
+            $encoder = new $encoderClass();
+            $options = array();
+            if (isset($extraOptions['decode_php_objs']) && $extraOptions['decode_php_objs']) {
+                $options[] = 'decode_php_objs';
+            }
+            $params = $encoder->decode($req, $options);
+
+            $result = call_user_func_array($callable, $params);
+
+            if (! is_a($result, $responseClass)) {
+                // q: why not do the same for int, float, bool, string?
+                if ($funcDesc['returns'] == Value::$xmlrpcDateTime || $funcDesc['returns'] == Value::$xmlrpcBase64) {
+                    $result = new $valueClass($result, $funcDesc['returns']);
+                } else {
+                    $options = array();
+                    if (isset($extraOptions['encode_php_objs']) && $extraOptions['encode_php_objs']) {
+                        $options[] = 'encode_php_objs';
+                    }
+                    if (isset($extraOptions['encode_nulls']) && $extraOptions['encode_nulls']) {
+                        $options[] = 'null_extension';
+                    }
+
+                    $result = $encoder->encode($result, $options);
+                }
+                $result = new $responseClass($result);
+            }
+
+            return $result;
+        };
+
+        return $function;
+    }
+
+    /**
+     * Return a name for a new function, based on $callable, insuring its uniqueness
+     * @param mixed $callable a php callable, or the name of an xml-rpc method
+     * @param string $newFuncName when not empty, it is used instead of the calculated version
+     * @return string
+     */
+    protected function newFunctionName($callable, $newFuncName, $extraOptions)
+    {
+        // determine name of new php function
+
+        $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : 'xmlrpc';
+
+        if ($newFuncName == '') {
+            if (is_array($callable)) {
+                if (is_string($callable[0])) {
+                    $xmlrpcFuncName = "{$prefix}_" . implode('_', $callable);
+                } else {
+                    $xmlrpcFuncName = "{$prefix}_" . get_class($callable[0]) . '_' . $callable[1];
+                }
+            } else {
+                if ($callable instanceof \Closure) {
+                    $xmlrpcFuncName = "{$prefix}_closure";
+                } else {
+                    $callable = preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'),
+                        array('_', ''), $callable);
+                    $xmlrpcFuncName = "{$prefix}_$callable";
+                }
+            }
+        } else {
+            $xmlrpcFuncName = $newFuncName;
+        }
+
+        while (function_exists($xmlrpcFuncName)) {
+            $xmlrpcFuncName .= 'x';
+        }
+
+        return $xmlrpcFuncName;
+    }
+
+    /**
+     * @param $callable
+     * @param string $newFuncName
+     * @param array $extraOptions
+     * @param string $plainFuncName
+     * @param array $funcDesc
+     * @return string
+     */
+    protected function buildWrapFunctionSource($callable, $newFuncName, $extraOptions, $plainFuncName, $funcDesc)
+    {
+        $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false;
+        $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
+        $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
+        $catchWarnings = isset($extraOptions['suppress_warnings']) && $extraOptions['suppress_warnings'] ? '@' : '';
+
+        $i = 0;
+        $parsVariations = array();
+        $pars = array();
+        $pNum = count($funcDesc['params']);
+        foreach ($funcDesc['params'] as $param) {
+
+            if ($param['isoptional']) {
+                // this particular parameter is optional. save as valid previous list of parameters
+                $parsVariations[] = $pars;
+            }
+
+            $pars[] = "\$params[$i]";
+            $i++;
+            if ($i == $pNum) {
+                // last allowed parameters combination
+                $parsVariations[] = $pars;
+            }
+        }
+
+        if (count($parsVariations) == 0) {
+            // only known good synopsis = no parameters
+            $parsVariations[] = array();
+            $minPars = 0;
+            $maxPars = 0;
+        } else {
+            $minPars = count($parsVariations[0]);
+            $maxPars = count($parsVariations[count($parsVariations)-1]);
+        }
+
+        // build body of new function
+
+        $innerCode = "  \$paramCount = \$req->getNumParams();\n";
+        $innerCode .= "  if (\$paramCount < $minPars || \$paramCount > $maxPars) return new " . static::$namespace . "Response(0, " . PhpXmlRpc::$xmlrpcerr['incorrect_params'] . ", '" . PhpXmlRpc::$xmlrpcstr['incorrect_params'] . "');\n";
+
+        $innerCode .= "  \$encoder = new " . static::$namespace . "Encoder();\n";
+        if ($decodePhpObjects) {
+            $innerCode .= "  \$params = \$encoder->decode(\$req, array('decode_php_objs'));\n";
+        } else {
+            $innerCode .= "  \$params = \$encoder->decode(\$req);\n";
+        }
+
+        // since we are building source code for later use, if we are given an object instance,
+        // we go out of our way and store a pointer to it in a static class var...
+        if (is_array($callable) && is_object($callable[0])) {
+            static::holdObject($newFuncName, $callable[0]);
+            $class = get_class($callable[0]);
+            if ($class[0] !== '\\') {
+                $class = '\\' . $class;
+            }
+            $innerCode .= "  /// @var $class \$obj\n";
+            $innerCode .= "  \$obj = PhpXmlRpc\\Wrapper::getHeldObject('$newFuncName');\n";
+            $realFuncName = '$obj->' . $callable[1];
+        } else {
+            $realFuncName = $plainFuncName;
+        }
+        foreach ($parsVariations as $i => $pars) {
+            $innerCode .= "  if (\$paramCount == " . count($pars) . ") \$retVal = {$catchWarnings}$realFuncName(" . implode(',', $pars) . ");\n";
+            if ($i < (count($parsVariations) - 1))
+                $innerCode .= "  else\n";
+        }
+        $innerCode .= "  if (is_a(\$retVal, '" . static::$namespace . "Response'))\n    return \$retVal;\n  else\n";
+        /// q: why not do the same for int, float, bool, string?
+        if ($funcDesc['returns'] == Value::$xmlrpcDateTime || $funcDesc['returns'] == Value::$xmlrpcBase64) {
+            $innerCode .= "    return new " . static::$namespace . "Response(new " . static::$namespace . "Value(\$retVal, '{$funcDesc['returns']}'));";
+        } else {
+            $encodeOptions = array();
+            if ($encodeNulls) {
+                $encodeOptions[] = 'null_extension';
+            }
+            if ($encodePhpObjects) {
+                $encodeOptions[] = 'encode_php_objs';
+            }
+
+            if ($encodeOptions) {
+                $innerCode .= "    return new " . static::$namespace . "Response(\$encoder->encode(\$retVal, array('" .
+                    implode("', '", $encodeOptions) . "')));";
+            } else {
+                $innerCode .= "    return new " . static::$namespace . "Response(\$encoder->encode(\$retVal));";
+            }
+        }
+        // shall we exclude functions returning by ref?
+        // if ($func->returnsReference())
+        //     return false;
+
+        $code = "/**\n * @param \PhpXmlRpc\Request \$req\n * @return \PhpXmlRpc\Response\n * @throws \\Exception\n */\n" .
+            "function $newFuncName(\$req)\n{\n" . $innerCode . "\n}";
+
+        return $code;
+    }
+
+    /**
+     * Given a user-defined PHP class or php object, map its methods onto a list of
+     * PHP 'wrapper' functions that can be exposed as xml-rpc methods from an xml-rpc server
+     * object and called from remote clients (as well as their corresponding signature info).
+     *
+     * @param string|object $className the name of the class whose methods are to be exposed as xml-rpc methods, or an object instance of that class
+     * @param array $extraOptions see the docs for wrapPhpFunction for basic options, plus
+     *                            - string method_type    'static', 'nonstatic', 'all' and 'auto' (default); the latter will switch between static and non-static depending on whether $className is a class name or object instance
+     *                            - string method_filter  a regexp used to filter methods to wrap based on their names
+     *                            - string prefix         used for the names of the xml-rpc methods created.
+     *                            - string replace_class_name use to completely replace the class name with the prefix in the generated method names. e.g. instead of \Some\Namespace\Class.method use prefixmethod
+     * @return array|false false on failure, or on array useable for the dispatch map
+     *
+     * @todo allow the generated function to be able to reuse an external Encoder instance instead of creating one on
+     *       each invocation, for the case where all the generated functions will be saved as methods of a class
+     */
+    public function wrapPhpClass($className, $extraOptions = array())
+    {
+        $methodFilter = isset($extraOptions['method_filter']) ? $extraOptions['method_filter'] : '';
+        $methodType = isset($extraOptions['method_type']) ? $extraOptions['method_type'] : 'auto';
+
+        $results = array();
+        $mList = get_class_methods($className);
+        foreach ($mList as $mName) {
+            if ($methodFilter == '' || preg_match($methodFilter, $mName)) {
+                $func = new \ReflectionMethod($className, $mName);
+                if (!$func->isPrivate() && !$func->isProtected() && !$func->isConstructor() && !$func->isDestructor() && !$func->isAbstract()) {
+                    if (($func->isStatic() && ($methodType == 'all' || $methodType == 'static' || ($methodType == 'auto' && is_string($className)))) ||
+                        (!$func->isStatic() && ($methodType == 'all' || $methodType == 'nonstatic' || ($methodType == 'auto' && is_object($className))))
+                    ) {
+                        $methodWrap = $this->wrapPhpFunction(array($className, $mName), '', $extraOptions);
+
+                        if ($methodWrap) {
+                            $results[$this->generateMethodNameForClassMethod($className, $mName, $extraOptions)] = $methodWrap;
+                        }
+                    }
+                }
+            }
+        }
+
+        return $results;
+    }
+
+    /**
+     * @param string|object $className
+     * @param string $classMethod
+     * @param array $extraOptions
+     * @return string
+     *
+     * @todo php allows many more characters in identifiers than the xml-rpc spec does. We should make sure to
+     *       replace those (while trying to make sure we are not running in collisions)
+     */
+    protected function generateMethodNameForClassMethod($className, $classMethod, $extraOptions = array())
+    {
+        if (isset($extraOptions['replace_class_name']) && $extraOptions['replace_class_name']) {
+            return (isset($extraOptions['prefix']) ?  $extraOptions['prefix'] : '') . $classMethod;
+        }
+
+        if (is_object($className)) {
+            $realClassName = get_class($className);
+        } else {
+            $realClassName = $className;
+        }
+        return (isset($extraOptions['prefix']) ?  $extraOptions['prefix'] : '') . "$realClassName.$classMethod";
+    }
+
+    /**
+     * Given an xml-rpc client and a method name, register a php wrapper function that will call it and return results
+     * using native php types for both arguments and results. The generated php function will return a Response
+     * object for failed xml-rpc calls.
+     *
+     * Known limitations:
+     * - server must support system.methodSignature for the target xml-rpc method
+     * - for methods that expose many signatures, only one can be picked (we could in principle check if signatures
+     *   differ only by number of params and not by type, but it would be more complication than we can spare time for)
+     * - nested xml-rpc params: the caller of the generated php function has to encode on its own the params passed to
+     *   the php function if these are structs or arrays whose (sub)members include values of type base64
+     *
+     * Notes: the connection properties of the given client will be copied and reused for the connection used during
+     * the call to the generated php function.
+     * Calling the generated php function 'might' be slightly slow: a new xml-rpc client is created on every invocation
+     * and an xmlrpc-connection opened+closed.
+     * An extra 'debug' argument, defaulting to 0, is appended to the argument list of the generated function, useful
+     * for debugging purposes.
+     *
+     * @param Client $client an xml-rpc client set up correctly to communicate with target server
+     * @param string $methodName the xml-rpc method to be mapped to a php function
+     * @param array $extraOptions array of options that specify conversion details. Valid options include
+     *                            - integer signum              the index of the method signature to use in mapping (if
+     *                                                          method exposes many sigs)
+     *                            - integer timeout             timeout (in secs) to be used when executing function/calling remote method
+     *                            - string  protocol            'http' (default), 'http11', 'https', 'h2' or 'h2c'
+     *                            - string  new_function_name   the name of php function to create, when return_source is used.
+     *                                                          If unspecified, lib will pick an appropriate name
+     *                            - string  return_source       if true return php code w. function definition instead of
+     *                                                          the function itself (closure)
+     *                            - bool    encode_nulls        if true, use `<nil/>` elements instead of empty string xml-rpc
+     *                                                          values for php null values
+     *                            - bool    encode_php_objs     let php objects be sent to server using the 'improved' xml-rpc
+     *                                                          notation, so server can deserialize them as php objects
+     *                            - bool    decode_php_objs     --- WARNING !!! possible security hazard. only use it with
+     *                                                          trusted servers ---
+     *                            - mixed   return_on_fault     a php value to be returned when the xml-rpc call fails/returns
+     *                                                          a fault response (by default the Response object is returned
+     *                                                          in this case).  If a string is used, '%faultCode%' and
+     *                                                          '%faultString%' tokens  will be substituted with actual error values
+     *                            - bool    throw_on_fault      if true, throw an exception instead of returning a Response
+     *                                                          in case of errors/faults;
+     *                                                          if a string, do the same and assume it is the exception class to throw
+     *                            - bool    debug               set it to 1 or 2 to see debug results of querying server for
+     *                                                          method synopsis
+     *                            - int     simple_client_copy  set it to 1 to have a lightweight copy of the $client object
+     *                                                          made in the generated code (only used when return_source = true)
+     * @return \Closure|string[]|false false on failure, closure by default and array for return_source = true
+     *
+     * @todo allow caller to give us the method signature instead of querying for it, or just say 'skip it'
+     * @todo if we can not retrieve method signature, create a php function with varargs
+     * @todo if caller did not specify a specific sig, shall we support all of them?
+     *       It might be hard (hence slow) to match based on type and number of arguments...
+     * @todo when wrapping methods without obj rebuilding, use return_type = 'phpvals' (faster)
+     * @todo allow creating functions which have an extra `$debug=0` parameter
+     */
+    public function wrapXmlrpcMethod($client, $methodName, $extraOptions = array())
+    {
+        $newFuncName = isset($extraOptions['new_function_name']) ? $extraOptions['new_function_name'] : '';
+
+        $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true;
+
+        $mSig = $this->retrieveMethodSignature($client, $methodName, $extraOptions);
+        if (!$mSig) {
+            return false;
+        }
+
+        if ($buildIt) {
+            return $this->buildWrapMethodClosure($client, $methodName, $extraOptions, $mSig);
+        } else {
+            // if in 'offline' mode, retrieve method description too.
+            // in online mode, favour speed of operation
+            $mDesc = $this->retrieveMethodHelp($client, $methodName, $extraOptions);
+
+            $newFuncName = $this->newFunctionName($methodName, $newFuncName, $extraOptions);
+
+            $results = $this->buildWrapMethodSource($client, $methodName, $extraOptions, $newFuncName, $mSig, $mDesc);
+
+            $results['function'] = $newFuncName;
+
+            return $results;
+        }
+    }
+
+    /**
+     * Retrieves an xml-rpc method signature from a server which supports system.methodSignature
+     * @param Client $client
+     * @param string $methodName
+     * @param array $extraOptions
+     * @return false|array
+     */
+    protected function retrieveMethodSignature($client, $methodName, array $extraOptions = array())
+    {
+        $reqClass = static::$namespace . 'Request';
+        $valClass = static::$namespace . 'Value';
+        $decoderClass = static::$namespace . 'Encoder';
+
+        $debug = isset($extraOptions['debug']) ? ($extraOptions['debug']) : 0;
+        $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
+        $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
+        $sigNum = isset($extraOptions['signum']) ? (int)$extraOptions['signum'] : 0;
+
+        $req = new $reqClass('system.methodSignature');
+        $req->addParam(new $valClass($methodName));
+        $origDebug = $client->getOption(Client::OPT_DEBUG);
+        $client->setDebug($debug);
+        /// @todo move setting of timeout, protocol to outside the send() call
+        $response = $client->send($req, $timeout, $protocol);
+        $client->setDebug($origDebug);
+        if ($response->faultCode()) {
+            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve method signature from remote server for method ' . $methodName);
+            return false;
+        }
+
+        $mSig = $response->value();
+        /// @todo what about return xml?
+        if ($client->getOption(Client::OPT_RETURN_TYPE) != 'phpvals') {
+            $decoder = new $decoderClass();
+            $mSig = $decoder->decode($mSig);
+        }
+
+        if (!is_array($mSig) || count($mSig) <= $sigNum) {
+            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve method signature nr.' . $sigNum . ' from remote server for method ' . $methodName);
+            return false;
+        }
+
+        return $mSig[$sigNum];
+    }
+
+    /**
+     * @param Client $client
+     * @param string $methodName
+     * @param array $extraOptions
+     * @return string in case of any error, an empty string is returned, no warnings generated
+     */
+    protected function retrieveMethodHelp($client, $methodName, array $extraOptions = array())
+    {
+        $reqClass = static::$namespace . 'Request';
+        $valClass = static::$namespace . 'Value';
+
+        $debug = isset($extraOptions['debug']) ? ($extraOptions['debug']) : 0;
+        $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
+        $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
+
+        $mDesc = '';
+
+        $req = new $reqClass('system.methodHelp');
+        $req->addParam(new $valClass($methodName));
+        $origDebug = $client->getOption(Client::OPT_DEBUG);
+        $client->setDebug($debug);
+        /// @todo move setting of timeout, protocol to outside the send() call
+        $response = $client->send($req, $timeout, $protocol);
+        $client->setDebug($origDebug);
+        if (!$response->faultCode()) {
+            $mDesc = $response->value();
+            if ($client->getOption(Client::OPT_RETURN_TYPE) != 'phpvals') {
+                $mDesc = $mDesc->scalarVal();
+            }
+        }
+
+        return $mDesc;
+    }
+
+    /**
+     * @param Client $client
+     * @param string $methodName
+     * @param array $extraOptions @see wrapXmlrpcMethod
+     * @param array $mSig
+     * @return \Closure
+     *
+     * @todo should we allow usage of parameter simple_client_copy to mean 'do not clone' in this case?
+     */
+    protected function buildWrapMethodClosure($client, $methodName, array $extraOptions, $mSig)
+    {
+        // we clone the client, so that we can modify it a bit independently of the original
+        $clientClone = clone $client;
+        $function = function() use($clientClone, $methodName, $extraOptions, $mSig)
+        {
+            $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
+            $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
+            $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
+            $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
+            $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false;
+            $throwFault = false;
+            $decodeFault = false;
+            $faultResponse = null;
+            if (isset($extraOptions['throw_on_fault'])) {
+                $throwFault = $extraOptions['throw_on_fault'];
+            } else if (isset($extraOptions['return_on_fault'])) {
+                $decodeFault = true;
+                $faultResponse = $extraOptions['return_on_fault'];
+            }
+
+            $reqClass = static::$namespace . 'Request';
+            $encoderClass = static::$namespace . 'Encoder';
+            $valueClass = static::$namespace . 'Value';
+
+            $encoder = new $encoderClass();
+            $encodeOptions = array();
+            if ($encodePhpObjects) {
+                $encodeOptions[] = 'encode_php_objs';
+            }
+            if ($encodeNulls) {
+                $encodeOptions[] = 'null_extension';
+            }
+            $decodeOptions = array();
+            if ($decodePhpObjects) {
+                $decodeOptions[] = 'decode_php_objs';
+            }
+
+            /// @todo check for insufficient nr. of args besides excess ones? note that 'source' version does not...
+
+            // support one extra parameter: debug
+            $maxArgs = count($mSig)-1; // 1st element is the return type
+            $currentArgs = func_get_args();
+            if (func_num_args() == ($maxArgs+1)) {
+                $debug = array_pop($currentArgs);
+                $clientClone->setDebug($debug);
+            }
+
+            $xmlrpcArgs = array();
+            foreach ($currentArgs as $i => $arg) {
+                if ($i == $maxArgs) {
+                    break;
+                }
+                $pType = $mSig[$i+1];
+                if ($pType == 'i4' || $pType == 'i8' || $pType == 'int' || $pType == 'boolean' || $pType == 'double' ||
+                    $pType == 'string' || $pType == 'dateTime.iso8601' || $pType == 'base64' || $pType == 'null'
+                ) {
+                    // by building directly xml-rpc values when type is known and scalar (instead of encode() calls),
+                    // we make sure to honour the xml-rpc signature
+                    $xmlrpcArgs[] = new $valueClass($arg, $pType);
+                } else {
+                    $xmlrpcArgs[] = $encoder->encode($arg, $encodeOptions);
+                }
+            }
+
+            $req = new $reqClass($methodName, $xmlrpcArgs);
+            // use this to get the maximum decoding flexibility
+            $clientClone->setOption(Client::OPT_RETURN_TYPE, 'xmlrpcvals');
+            $resp = $clientClone->send($req, $timeout, $protocol);
+            if ($resp->faultcode()) {
+                if ($throwFault) {
+                    if (is_string($throwFault)) {
+                        throw new $throwFault($resp->faultString(), $resp->faultCode());
+                    } else {
+                        throw new \PhpXmlRpc\Exception($resp->faultString(), $resp->faultCode());
+                    }
+                } else if ($decodeFault) {
+                    if (is_string($faultResponse) && ((strpos($faultResponse, '%faultCode%') !== false) ||
+                            (strpos($faultResponse, '%faultString%') !== false))) {
+                        $faultResponse = str_replace(array('%faultCode%', '%faultString%'),
+                            array($resp->faultCode(), $resp->faultString()), $faultResponse);
+                    }
+                    return $faultResponse;
+                } else {
+                    return $resp;
+                }
+            } else {
+                return $encoder->decode($resp->value(), $decodeOptions);
+            }
+        };
+
+        return $function;
+    }
+
+    /**
+     * @internal made public just for Debugger usage
+     *
+     * @param Client $client
+     * @param string $methodName
+     * @param array $extraOptions @see wrapXmlrpcMethod
+     * @param string $newFuncName
+     * @param array $mSig
+     * @param string $mDesc
+     * @return string[] keys: source, docstring
+     */
+    public function buildWrapMethodSource($client, $methodName, array $extraOptions, $newFuncName, $mSig, $mDesc='')
+    {
+        $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
+        $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
+        $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
+        $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
+        $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false;
+        $clientCopyMode = isset($extraOptions['simple_client_copy']) ? (int)($extraOptions['simple_client_copy']) : 0;
+        $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : 'xmlrpc';
+        $throwFault = false;
+        $decodeFault = false;
+        $faultResponse = null;
+        if (isset($extraOptions['throw_on_fault'])) {
+            $throwFault = $extraOptions['throw_on_fault'];
+        } else if (isset($extraOptions['return_on_fault'])) {
+            $decodeFault = true;
+            $faultResponse = $extraOptions['return_on_fault'];
+        }
+
+        $code = "function $newFuncName(";
+        if ($clientCopyMode < 2) {
+            // client copy mode 0 or 1 == full / partial client copy in emitted code
+            $verbatimClientCopy = !$clientCopyMode;
+            $innerCode = '  ' . str_replace("\n", "\n  ", $this->buildClientWrapperCode($client, $verbatimClientCopy, $prefix, static::$namespace));
+            $innerCode .= "\$client->setDebug(\$debug);\n";
+            $this_ = '';
+        } else {
+            // client copy mode 2 == no client copy in emitted code
+            $innerCode = '';
+            $this_ = 'this->';
+        }
+        $innerCode .= "  \$req = new " . static::$namespace . "Request('$methodName');\n";
+
+        if ($mDesc != '') {
+            // take care that PHP comment is not terminated unwillingly by method description
+            /// @todo according to the spec, method desc can have html in it. We should run it through strip_tags...
+            $mDesc = "/**\n * " . str_replace(array("\n", '*/'), array("\n * ", '* /'), $mDesc) . "\n";
+        } else {
+            $mDesc = "/**\n * Function $newFuncName.\n";
+        }
+
+        // param parsing
+        $innerCode .= "  \$encoder = new " . static::$namespace . "Encoder();\n";
+        $plist = array();
+        $pCount = count($mSig);
+        for ($i = 1; $i < $pCount; $i++) {
+            $plist[] = "\$p$i";
+            $pType = $mSig[$i];
+            if ($pType == 'i4' || $pType == 'i8' || $pType == 'int' || $pType == 'boolean' || $pType == 'double' ||
+                $pType == 'string' || $pType == 'dateTime.iso8601' || $pType == 'base64' || $pType == 'null'
+            ) {
+                // only build directly xml-rpc values when type is known and scalar
+                $innerCode .= "  \$p$i = new " . static::$namespace . "Value(\$p$i, '$pType');\n";
+            } else {
+                if ($encodePhpObjects || $encodeNulls) {
+                    $encOpts = array();
+                    if ($encodePhpObjects) {
+                        $encOpts[] = 'encode_php_objs';
+                    }
+                    if ($encodeNulls) {
+                        $encOpts[] = 'null_extension';
+                    }
+
+                    $innerCode .= "  \$p$i = \$encoder->encode(\$p$i, array( '" . implode("', '", $encOpts) . "'));\n";
+                } else {
+                    $innerCode .= "  \$p$i = \$encoder->encode(\$p$i);\n";
+                }
+            }
+            $innerCode .= "  \$req->addParam(\$p$i);\n";
+            $mDesc .= " * @param " . $this->xmlrpc2PhpType($pType) . " \$p$i\n";
+        }
+        if ($clientCopyMode < 2) {
+            $plist[] = '$debug = 0';
+            $mDesc .= " * @param int \$debug when 1 (or 2) will enable debugging of the underlying {$prefix} call (defaults to 0)\n";
+        }
+        $plist = implode(', ', $plist);
+        $mDesc .= ' * @return ' . $this->xmlrpc2PhpType($mSig[0]);
+        if ($throwFault) {
+            $mDesc .= "\n * @throws " . (is_string($throwFault) ? $throwFault : '\\PhpXmlRpc\\Exception');
+        } else if ($decodeFault) {
+            $mDesc .= '|' . gettype($faultResponse) . " (a " . gettype($faultResponse) . " if call fails)";
+        } else {
+            $mDesc .= '|' . static::$namespace . "Response (a " . static::$namespace . "Response obj instance if call fails)";
+        }
+        $mDesc .= "\n */\n";
+
+        /// @todo move setting of timeout, protocol to outside the send() call
+        $innerCode .= "  \$res = \${$this_}client->send(\$req, $timeout, '$protocol');\n";
+        if ($throwFault) {
+            if (!is_string($throwFault)) {
+                $throwFault = '\\PhpXmlRpc\\Exception';
+            }
+            $respCode = "throw new $throwFault(\$res->faultString(), \$res->faultCode())";
+        } else if ($decodeFault) {
+            if (is_string($faultResponse) && ((strpos($faultResponse, '%faultCode%') !== false) || (strpos($faultResponse, '%faultString%') !== false))) {
+                $respCode = "return str_replace(array('%faultCode%', '%faultString%'), array(\$res->faultCode(), \$res->faultString()), '" . str_replace("'", "''", $faultResponse) . "')";
+            } else {
+                $respCode = 'return ' . var_export($faultResponse, true);
+            }
+        } else {
+            $respCode = 'return $res';
+        }
+        if ($decodePhpObjects) {
+            $innerCode .= "  if (\$res->faultCode()) $respCode; else return \$encoder->decode(\$res->value(), array('decode_php_objs'));";
+        } else {
+            $innerCode .= "  if (\$res->faultCode()) $respCode; else return \$encoder->decode(\$res->value());";
+        }
+
+        $code = $code . $plist . ")\n{\n" . $innerCode . "\n}\n";
+
+        return array('source' => $code, 'docstring' => $mDesc);
+    }
+
+    /**
+     * Similar to wrapXmlrpcMethod, but will generate a php class that wraps all xml-rpc methods exposed by the remote
+     * server as own methods.
+     * For a slimmer alternative, see the code in demo/client/proxy.php.
+     * Note that unlike wrapXmlrpcMethod, we always have to generate php code here. Since php 7 anon classes exist, but
+     * we do not support them yet...
+     *
+     * @see wrapXmlrpcMethod for more details.
+     *
+     * @param Client $client the client obj all set to query the desired server
+     * @param array $extraOptions list of options for wrapped code. See the ones from wrapXmlrpcMethod, plus
+     *                            - string method_filter      regular expression
+     *                            - string new_class_name
+     *                            - string prefix
+     *                            - bool   simple_client_copy set it to true to avoid copying all properties of $client into the copy made in the new class
+     * @return string|array|false false on error, the name of the created class if all ok or an array with code, class name and comments (if the appropriate option is set in extra_options)
+     *
+     * @todo add support for anonymous classes in the 'buildIt' case for php > 7
+     * @todo add method setDebug() to new class, to enable/disable debugging
+     * @todo optimization - move the generated Encoder instance to be a property of the created class, instead of creating
+     *                      it on every generated method invocation
+     */
+    public function wrapXmlrpcServer($client, $extraOptions = array())
+    {
+        $methodFilter = isset($extraOptions['method_filter']) ? $extraOptions['method_filter'] : '';
+        $timeout = isset($extraOptions['timeout']) ? (int)$extraOptions['timeout'] : 0;
+        $protocol = isset($extraOptions['protocol']) ? $extraOptions['protocol'] : '';
+        $newClassName = isset($extraOptions['new_class_name']) ? $extraOptions['new_class_name'] : '';
+        $encodeNulls = isset($extraOptions['encode_nulls']) ? (bool)$extraOptions['encode_nulls'] : false;
+        $encodePhpObjects = isset($extraOptions['encode_php_objs']) ? (bool)$extraOptions['encode_php_objs'] : false;
+        $decodePhpObjects = isset($extraOptions['decode_php_objs']) ? (bool)$extraOptions['decode_php_objs'] : false;
+        $verbatimClientCopy = isset($extraOptions['simple_client_copy']) ? !($extraOptions['simple_client_copy']) : true;
+        $throwOnFault = isset($extraOptions['throw_on_fault']) ? (bool)$extraOptions['throw_on_fault'] : false;
+        $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true;
+        $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : 'xmlrpc';
+
+        $reqClass = static::$namespace . 'Request';
+        $decoderClass = static::$namespace . 'Encoder';
+
+        // retrieve the list of methods
+        $req = new $reqClass('system.listMethods');
+        /// @todo move setting of timeout, protocol to outside the send() call
+        $response = $client->send($req, $timeout, $protocol);
+        if ($response->faultCode()) {
+            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve method list from remote server');
+
+            return false;
+        }
+        $mList = $response->value();
+        /// @todo what about return_type = xml?
+        if ($client->getOption(Client::OPT_RETURN_TYPE) != 'phpvals') {
+            $decoder = new $decoderClass();
+            $mList = $decoder->decode($mList);
+        }
+        if (!is_array($mList) || !count($mList)) {
+            $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not retrieve meaningful method list from remote server');
+
+            return false;
+        }
+
+        // pick a suitable name for the new function, avoiding collisions
+        if ($newClassName != '') {
+            $xmlrpcClassName = $newClassName;
+        } else {
+            /// @todo direct access to $client->server is now deprecated
+            $xmlrpcClassName = $prefix . '_' . preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'), array('_', ''),
+                $client->server) . '_client';
+        }
+        while ($buildIt && class_exists($xmlrpcClassName)) {
+            $xmlrpcClassName .= 'x';
+        }
+
+        $source = "class $xmlrpcClassName\n{\n  public \$client;\n\n";
+        $source .= "  function __construct()\n  {\n";
+        $source .= '    ' . str_replace("\n", "\n    ", $this->buildClientWrapperCode($client, $verbatimClientCopy, $prefix, static::$namespace));
+        $source .= "\$this->client = \$client;\n  }\n\n";
+        $opts = array(
+            'return_source' => true,
+            'simple_client_copy' => 2, // do not produce code to copy the client object
+            'timeout' => $timeout,
+            'protocol' => $protocol,
+            'encode_nulls' => $encodeNulls,
+            'encode_php_objs' => $encodePhpObjects,
+            'decode_php_objs' => $decodePhpObjects,
+            'throw_on_fault' => $throwOnFault,
+            'prefix' => $prefix,
+        );
+
+        /// @todo build phpdoc for class definition, too
+        foreach ($mList as $mName) {
+            if ($methodFilter == '' || preg_match($methodFilter, $mName)) {
+                /// @todo this will fail if server exposes 2 methods called f.e. do.something and do_something
+                $opts['new_function_name'] = preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'),
+                    array('_', ''), $mName);
+                $methodWrap = $this->wrapXmlrpcMethod($client, $mName, $opts);
+                if ($methodWrap) {
+                    if ($buildIt) {
+                        $source .= $methodWrap['source'] . "\n";
+
+                    } else {
+                        $source .= '  ' . str_replace("\n", "\n  ", $methodWrap['docstring']);
+                        $source .= str_replace("\n", "\n  ", $methodWrap['source']). "\n";
+                    }
+
+                } else {
+                    $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': will not create class method to wrap remote method ' . $mName);
+                }
+            }
+        }
+        $source .= "}\n";
+        if ($buildIt) {
+            $allOK = 0;
+            eval($source . '$allOK=1;');
+            if ($allOK) {
+                return $xmlrpcClassName;
+            } else {
+                /// @todo direct access to $client->server is now deprecated
+                $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': could not create class ' . $xmlrpcClassName .
+                    ' to wrap remote server ' . $client->server);
+                return false;
+            }
+        } else {
+            return array('class' => $xmlrpcClassName, 'code' => $source, 'docstring' => '');
+        }
+    }
+
+    /**
+     * Given necessary info, generate php code that will build a client object just like the given one.
+     * Take care that no full checking of input parameters is done to ensure that valid php code is emitted.
+     * @param Client $client
+     * @param bool $verbatimClientCopy when true, copy the whole options of the client, except for 'debug' and 'return_type'
+     * @param string $prefix used for the return_type of the created client
+     * @param string $namespace
+     * @return string
+     */
+    protected function buildClientWrapperCode($client, $verbatimClientCopy, $prefix = 'xmlrpc', $namespace = '\\PhpXmlRpc\\')
+    {
+        $code = "\$client = new {$namespace}Client('" . str_replace(array("\\", "'"), array("\\\\", "\'"), $client->getUrl()) .
+            "');\n";
+
+        // copy all client fields to the client that will be generated runtime
+        // (this provides for future expansion or subclassing of client obj)
+        if ($verbatimClientCopy) {
+            foreach ($client->getOptions() as $opt => $val) {
+                if ($opt != 'debug' && $opt != 'return_type') {
+                    $val = var_export($val, true);
+                    $code .= "\$client->setOption('$opt', $val);\n";
+                }
+            }
+        }
+        // only make sure that client always returns the correct data type
+        $code .= "\$client->setOption(\PhpXmlRpc\Client::OPT_RETURN_TYPE, '{$prefix}vals');\n";
+        return $code;
+    }
+
+    /**
+     * @param string $index
+     * @param object $object
+     * @return void
+     */
+    public static function holdObject($index, $object)
+    {
+        self::$objHolder[$index] = $object;
+    }
+
+    /**
+     * @param string $index
+     * @return object
+     * @throws ValueErrorException
+     */
+    public static function getHeldObject($index)
+    {
+        if (isset(self::$objHolder[$index])) {
+            return self::$objHolder[$index];
+        }
+
+        throw new ValueErrorException("No object held for index '$index'");
+    }
+}

+ 0 - 1
install.php

@@ -235,7 +235,6 @@ function install() {
 
         /* TODO: how to check if pear is enabled or not? */
         $properties_to_check = array(
-            array( "name" => "PHP XML-RPC module", "type" => "f", "value" => "xmlrpc_server_create" ),
             array( "name" => "PHP Curl module", "type" => "f", "value" => "curl_init" ),
             array( "name" => "PHP XML Reader", "type" => "c", "value" => "XMLReader" ),
 			array( "name" => "PHP JSON Extension", "type" => "f", "value" => "json_decode" ),