<?php

/*
   XML-RPC Library for PHP

   This is Version 4.0.0 of the XML-RPC Library for PHP, which is compatible with PHP Version 7.0.
   It was downloaded from the website: http://gggeek.github.io/phpxmlrpc/
   It has been combined into a single PHP file, to make it easier to install.

   Original Author: http://gggeek.github.io/phpxmlrpc/

   Combined Into Single File By: Pav The Coder
   Combined Into Single File Date: September 2016

 */


namespace PhpXmlRpc
{

    use PhpXmlRpc\Helper\Charset;
    use PhpXmlRpc\Helper\Date;
    use PhpXmlRpc\Helper\Http;
    use PhpXmlRpc\Helper\Logger;
    use PhpXmlRpc\Helper\XMLParser;

    class PhpXmlRpc
    {
        static public $xmlrpcerr = array(
            'unknown_method' => 1,
            'invalid_return' => 2,
            'incorrect_params' => 3,
            'introspect_unknown' => 4,
            'http_error' => 5,
            'no_data' => 6,
            'no_ssl' => 7,
            'curl_fail' => 8,
            'invalid_request' => 15,
            'no_curl' => 16,
            'server_error' => 17,
            'multicall_error' => 18,
            'multicall_notstruct' => 9,
            'multicall_nomethod' => 10,
            'multicall_notstring' => 11,
            'multicall_recursion' => 12,
            'multicall_noparams' => 13,
            'multicall_notarray' => 14,

            'cannot_decompress' => 103,
            'decompress_fail' => 104,
            'dechunk_fail' => 105,
            'server_cannot_decompress' => 106,
            'server_decompress_fail' => 107,
        );

        static public $xmlrpcstr = array(
            'unknown_method' => 'Unknown method',
            'invalid_return' => 'Invalid return payload: enable debugging to examine incoming payload',
            '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',

            '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',
        );

        // 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";

        // The list of 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();

        // 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 xmlrpc methods
        public static $xmlrpc_internalencoding = "UTF-8";

        public static $xmlrpcName = "XML-RPC for PHP";
        public static $xmlrpcVersion = "4.0.0.alpha";

        // let user errors start at 800
        public static $xmlrpcerruser = 800;
        // let XML parse errors start at 100
        public static $xmlrpcerrxml = 100;

        // set to TRUE to enable correct decoding of <NIL/> and <EX:NIL/> values
        public static $xmlrpc_null_extension = false;

        // 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";

        /**
         * 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...
         */
        public static function exportGlobals()
        {
            $reflection = new \ReflectionClass('PhpXmlRpc\PhpXmlRpc');
            foreach ($reflection->getStaticProperties() as $name => $value) {
                $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) {
                $GLOBALS[$name] = $value;
            }

            $parser = new Helper\XMLParser();
            $reflection = new \ReflectionClass('PhpXmlRpc\Helper\XMLParser');
            foreach ($reflection->getProperties(\ReflectionProperty::IS_PUBLIC) as $name => $value) {
                if (in_array($value->getName(), array('xmlrpc_valid_parents')))
                {
                    $GLOBALS[$value->getName()] = $value->getValue($parser);
                }
            }

            $charset = Helper\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.
         */
        public static function importGlobals()
        {
            $reflection = new \ReflectionClass('PhpXmlRpc\PhpXmlRpc');
            $staticProperties = $reflection->getStaticProperties();
            foreach ($staticProperties as $name => $value) {
                if (isset($GLOBALS[$name])) {
                    self::$$name = $GLOBALS[$name];
                }
            }
        }

    }











    //use PhpXmlRpc\Helper\Logger;

    class Client
    {
        /// @todo: do these need to be public?
        public $method = 'http';
        public $server;
        public $port = 0;
        public $path;

        public $errno;
        public $errstr;
        public $debug = 0;

        public $username = '';
        public $password = '';
        public $authtype = 1;

        public $cert = '';
        public $certpass = '';
        public $cacert = '';
        public $cacertdir = '';
        public $key = '';
        public $keypass = '';
        public $verifypeer = true;
        public $verifyhost = 2;
        public $sslversion = 0; // corresponds to CURL_SSLVERSION_DEFAULT

        public $proxy = '';
        public $proxyport = 0;
        public $proxy_user = '';
        public $proxy_pass = '';
        public $proxy_authtype = 1;

        public $cookies = array();
        public $extracurlopts = array();

        public $no_multicall = false;

        /**
         * 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 wheter compression is supported or not
         */
        public $accepted_compression = array();
        /**
         * Name of compression scheme to be used for sending requests.
         * Either null, gzip or deflate.
         */
        public $request_compression = '';
        /**
         * CURL handle: used for keep-alive connections (PHP 4.3.8 up, see:
         * http://curl.haxx.se/docs/faq.html#7.3).
         */
        public $xmlrpc_curl_handle = null;
        /// Whether to use persistent connections for http 1.1 and https
        public $keepalive = false;
        /// Charset encodings that can be decoded without problems by the client
        public $accepted_charset_encodings = array();
        /// Charset encoding to be used in serializing request. NULL = use ASCII
        public $request_charset_encoding = '';
        /**
         * Decides the content of Response objects returned by calls to send()
         * valid strings are 'xmlrpcvals', 'phpvals' or 'xml'.
         */
        public $return_type = 'xmlrpcvals';
        /**
         * Sent to servers in http headers.
         */
        public $user_agent;

        /**
         * @param string $path either the complete server URL or the PATH part of the xmlrc server URL, e.g. /xmlrpc/server.php
         * @param string $server the server name / ip address
         * @param integer $port the port the server is listening on, defaults to 80 or 443 depending on protocol used
         * @param string $method the http protocol variant: defaults to 'http', 'https' and 'http11' can be used if CURL is installed
         */
        public function __construct($path, $server = '', $port = '', $method = '')
        {
            // allow user to specify all params in $path
            if ($server == '' and $port == '' and $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_init') && (($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');

            // Add all charsets which mbstring can handle, but remove junk not found in IANA registry at
            // in http://www.iana.org/assignments/character-sets/character-sets.xhtml
            // NB: this is disabled to avoid making all the requests sent huge... mbstring supports more than 80 charsets!
            /*if (function_exists('mb_list_encodings')) {

                $encodings = array_diff(mb_list_encodings(), array('pass', 'auto', 'wchar', 'BASE64', 'UUENCODE', 'ASCII',
                    'HTML-ENTITIES', 'Quoted-Printable', '7bit','8bit', 'byte2be', 'byte2le', 'byte4be', 'byte4le'));
                $this->accepted_charset_encodings = array_unique(array_merge($this->accepted_charset_encodings, $encodings));
            }*/

            // initialize user_agent string
            $this->user_agent = PhpXmlRpc::$xmlrpcName . ' ' . PhpXmlRpc::$xmlrpcVersion;
        }

        /**
         * Enables/disables the echoing to screen of the xmlrpc responses received.
         *
         * @param integer $in values 0, 1 and 2 are supported (2 = echo sent msg too, before received response)
         */
        public function setDebug($in)
        {
            $this->debug = $in;
        }

        /**
         * Add some http BASIC AUTH credentials, used by the client to authenticate.
         *
         * @param string $u username
         * @param string $p password
         * @param integer $t auth type. See curl_setopt man page for supported auth types. Defaults to CURLAUTH_BASIC (basic auth)
         */
        public function setCredentials($u, $p, $t = 1)
        {
            $this->username = $u;
            $this->password = $p;
            $this->authtype = $t;
        }

        /**
         * Add a client-side https certificate.
         *
         * @param string $cert
         * @param string $certPass
         */
        public function setCertificate($cert, $certPass)
        {
            $this->cert = $cert;
            $this->certpass = $certPass;
        }

        /**
         * Add a CA certificate to verify server with (see man 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
         */
        public function setCaCertificate($caCert, $isDir = false)
        {
            if ($isDir) {
                $this->cacertdir = $caCert;
            } else {
                $this->cacert = $caCert;
            }
        }

        /**
         * 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
         */
        public function setKey($key, $keyPass)
        {
            $this->key = $key;
            $this->keypass = $keyPass;
        }

        /**
         * Set attributes for SSL communication: verify server certificate.
         *
         * @param bool $i enable/disable verification of peer certificate
         */
        public function setSSLVerifyPeer($i)
        {
            $this->verifypeer = $i;
        }

        /**
         * Set attributes for SSL communication: verify match of server cert w. hostname.
         *
         * @param int $i
         */
        public function setSSLVerifyHost($i)
        {
            $this->verifyhost = $i;
        }

        /**
         * Set attributes for SSL communication: SSL version to use. Best left at 0 (default value ): let cURL decide
         *
         * @param int $i
         */
        public function setSSLVersion($i)
        {
            $this->sslversion = $i;
        }

        /**
         * Set proxy info.
         *
         * @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 set to constant CURLAUTH_NTLM to use NTLM auth with proxy
         */
        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;
        }

        /**
         * Enables/disables reception of compressed xmlrpc responses.
         * Note that enabling reception of compressed responses merely adds some standard
         * http headers to xmlrpc requests. It is up to the xmlrpc server to return
         * compressed responses when receiving such requests.
         *
         * @param string $compMethod either 'gzip', 'deflate', 'any' or ''
         */
        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);
            }
        }

        /**
         * Enables/disables http compression of xmlrpc request.
         * 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 ''
         */
        public function setRequestCompression($compMethod)
        {
            $this->request_compression = $compMethod;
        }

        /**
         * Adds a cookie to list of cookies that will be sent to server.
         * NB: setting any param but name and value will turn the cookie into a 'version 1' cookie:
         * do not do it unless you know what you are doing.
         *
         * @param string $name
         * @param string $value
         * @param string $path
         * @param string $domain
         * @param int $port
         *
         * @todo check correctness of urlencoding cookie value (copied from php way of doing it...)
         */
        public function setCookie($name, $value = '', $path = '', $domain = '', $port = null)
        {
            $this->cookies[$name]['value'] = urlencode($value);
            if ($path || $domain || $port) {
                $this->cookies[$name]['path'] = $path;
                $this->cookies[$name]['domain'] = $domain;
                $this->cookies[$name]['port'] = $port;
                $this->cookies[$name]['version'] = 1;
            } else {
                $this->cookies[$name]['version'] = 0;
            }
        }

        /**
         * Directly set cURL options, for extra flexibility
         * It allows eg. to bind client to a specific IP interface / address.
         *
         * @param array $options
         */
        public function SetCurlOptions($options)
        {
            $this->extracurlopts = $options;
        }

        /**
         * Set user-agent string that will be used by this client instance
         * in http headers sent to the server.
         *
         * @param string $agentString
         */
        public function SetUserAgent($agentString)
        {
            $this->user_agent = $agentString;
        }

        /**
         * Send an xmlrpc request.
         *
         * @param Request|Request[]|string $req The Request object, or an array of requests for using multicall, or the complete xml representation of a request
         * @param integer $timeout Connection timeout, in seconds, If unspecified, a platform specific timeout will apply
         * @param string $method if left unspecified, the http protocol chosen during creation of the object will be used
         *
         * @return Response|Response[]
         */
        public function send($req, $timeout = 0, $method = '')
        {
            // 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 (is_array($req)) {
                // $req is an array of Requests
                $r = $this->multicall($req, $timeout, $method);

                return $r;
            } elseif (is_string($req)) {
                $n = new Request('');
                $n->payload = $req;
                $req = $n;
            }

            // where req is a Request
            $req->setDebug($this->debug);

            if ($method == 'https') {
                $r = $this->sendPayloadHTTPS(
                    $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,
                    $this->keepalive,
                    $this->key,
                    $this->keypass,
                    $this->sslversion
                );
            } elseif ($method == 'http11') {
                $r = $this->sendPayloadCURL(
                    $req,
                    $this->server,
                    $this->port,
                    $timeout,
                    $this->username,
                    $this->password,
                    $this->authtype,
                    null,
                    null,
                    null,
                    null,
                    $this->proxy,
                    $this->proxyport,
                    $this->proxy_user,
                    $this->proxy_pass,
                    $this->proxy_authtype,
                    'http',
                    $this->keepalive
                );
            } else {
                $r = $this->sendPayloadHTTP10(
                    $req,
                    $this->server,
                    $this->port,
                    $timeout,
                    $this->username,
                    $this->password,
                    $this->authtype,
                    $this->proxy,
                    $this->proxyport,
                    $this->proxy_user,
                    $this->proxy_pass,
                    $this->proxy_authtype,
                    $method
                );
            }

            return $r;
        }

        /**
         * @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')
        {
            if ($port == 0) {
                $port = ( $method === "https" ) ? 443 : 80;
            }

            // Only create the payload if it was not created previously
            if (empty($req->payload)) {
                $req->createPayload($this->request_charset_encoding);
            }

            $payload = $req->payload;
            // Deflate request body and set appropriate request headers
            if (function_exists('gzdeflate') && ($this->request_compression == 'gzip' || $this->request_compression == 'deflate')) {
                if ($this->request_compression == 'gzip') {
                    $a = @gzencode($payload);
                    if ($a) {
                        $payload = $a;
                        $encodingHdr = "Content-Encoding: gzip\r\n";
                    }
                } else {
                    $a = @gzcompress($payload);
                    if ($a) {
                        $payload = $a;
                        $encodingHdr = "Content-Encoding: deflate\r\n";
                    }
                }
            } else {
                $encodingHdr = '';
            }

            // thanks to Grant Rauscher <grant7@firstworld.net> for this
            $credentials = '';
            if ($username != '') {
                $credentials = 'Authorization: Basic ' . base64_encode($username . ':' . $password) . "\r\n";
                if ($authType != 1) {
                    error_log('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth is supported with HTTP 1.0');
                }
            }

            $acceptedEncoding = '';
            if (is_array($this->accepted_compression) && count($this->accepted_compression)) {
                $acceptedEncoding = 'Accept-Encoding: ' . implode(', ', $this->accepted_compression) . "\r\n";
            }

            $proxyCredentials = '';
            if ($proxyHost) {
                if ($proxyPort == 0) {
                    $proxyPort = 8080;
                }
                $connectServer = $proxyHost;
                $connectPort = $proxyPort;
                $transport = "tcp";
                $uri = 'http://' . $server . ':' . $port . $this->path;
                if ($proxyUsername != '') {
                    if ($proxyAuthType != 1) {
                        error_log('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth to proxy is supported with HTTP 1.0');
                    }
                    $proxyCredentials = 'Proxy-Authorization: Basic ' . base64_encode($proxyUsername . ':' . $proxyPassword) . "\r\n";
                }
            } else {
                $connectServer = $server;
                $connectPort = $port;
                /// @todo if supporting https, we should support all its current options as well: peer name verification etc...
                $transport = ( $method === "https" ) ? "tls" : "tcp";
                $uri = $this->path;
            }

            // Cookie generation, as per rfc2965 (version 1 cookies) or
            // netscape's rules (version 0 cookies)
            $cookieHeader = '';
            if (count($this->cookies)) {
                $version = '';
                foreach ($this->cookies as $name => $cookie) {
                    if ($cookie['version']) {
                        $version = ' $Version="' . $cookie['version'] . '";';
                        $cookieHeader .= ' ' . $name . '="' . $cookie['value'] . '";';
                        if ($cookie['path']) {
                            $cookieHeader .= ' $Path="' . $cookie['path'] . '";';
                        }
                        if ($cookie['domain']) {
                            $cookieHeader .= ' $Domain="' . $cookie['domain'] . '";';
                        }
                        if ($cookie['port']) {
                            $cookieHeader .= ' $Port="' . $cookie['port'] . '";';
                        }
                    } else {
                        $cookieHeader .= ' ' . $name . '=' . $cookie['value'] . ";";
                    }
                }
                $cookieHeader = 'Cookie:' . $version . substr($cookieHeader, 0, -1) . "\r\n";
            }

            // omit port if 80
            $port = ($port == 80) ? '' : (':' . $port);

            $op = 'POST ' . $uri . " HTTP/1.0\r\n" .
                'User-Agent: ' . $this->user_agent . "\r\n" .
                'Host: ' . $server . $port . "\r\n" .
                $credentials .
                $proxyCredentials .
                $acceptedEncoding .
                $encodingHdr .
                'Accept-Charset: ' . implode(',', $this->accepted_charset_encodings) . "\r\n" .
                $cookieHeader .
                'Content-Type: ' . $req->content_type . "\r\nContent-Length: " .
                strlen($payload) . "\r\n\r\n" .
                $payload;

            if ($this->debug > 1) {
                Logger::instance()->debugMessage("---SENDING---\n$op\n---END---");
            }

            if ($timeout > 0) {
                $fp = @stream_socket_client("$transport://$connectServer:$connectPort", $this->errno, $this->errstr, $timeout);
            } else {
                $fp = @stream_socket_client("$transport://$connectServer:$connectPort", $this->errno, $this->errstr);
            }
            if ($fp) {
                if ($timeout > 0) {
                    stream_set_timeout($fp, $timeout);
                }
            } else {
                $this->errstr = 'Connect error: ' . $this->errstr;
                $r = new Response(0, PhpXmlRpc::$xmlrpcerr['http_error'], $this->errstr . ' (' . $this->errno . ')');

                return $r;
            }

            if (!fputs($fp, $op, strlen($op))) {
                fclose($fp);
                $this->errstr = 'Write error';
                $r = new Response(0, PhpXmlRpc::$xmlrpcerr['http_error'], $this->errstr);

                return $r;
            } else {
                // reset errno and errstr on successful socket connection
                $this->errstr = '';
            }
            // G. Giunta 2005/10/24: close socket before parsing.
            // 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);
            $r = $req->parseResponse($ipd, false, $this->return_type);

            return $r;
        }

        /**
         * @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)
        {
            return $this->sendPayloadCURL($req, $server, $port, $timeout, $username,
                $password, $authType, $cert, $certPass, $caCert, $caCertDir, $proxyHost, $proxyPort,
                $proxyUsername, $proxyPassword, $proxyAuthType, 'https', $keepAlive, $key, $keyPass, $sslVersion);
        }

        /**
         * Contributed by Justin Miller <justin@voxel.net>
         * 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 $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
         * @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)
        {
            if (!function_exists('curl_init')) {
                $this->errstr = 'CURL unavailable on this install';
                return new Response(0, PhpXmlRpc::$xmlrpcerr['no_curl'], PhpXmlRpc::$xmlrpcstr['no_curl']);
            }
            if ($method == 'https') {
                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 Response(0, PhpXmlRpc::$xmlrpcerr['no_ssl'], PhpXmlRpc::$xmlrpcstr['no_ssl']);
                }
            }

            if ($port == 0) {
                if ($method == 'http') {
                    $port = 80;
                } else {
                    $port = 443;
                }
            }

            // Only create the payload if it was not created previously
            if (empty($req->payload)) {
                $req->createPayload($this->request_charset_encoding);
            }

            // Deflate request body and set appropriate request headers
            $payload = $req->payload;
            if (function_exists('gzdeflate') && ($this->request_compression == 'gzip' || $this->request_compression == 'deflate')) {
                if ($this->request_compression == 'gzip') {
                    $a = @gzencode($payload);
                    if ($a) {
                        $payload = $a;
                        $encodingHdr = 'Content-Encoding: gzip';
                    }
                } else {
                    $a = @gzcompress($payload);
                    if ($a) {
                        $payload = $a;
                        $encodingHdr = 'Content-Encoding: deflate';
                    }
                }
            } else {
                $encodingHdr = '';
            }

            if ($this->debug > 1) {
                Logger::instance()->debugMessage("---SENDING---\n$payload\n---END---");
            }

            if (!$keepAlive || !$this->xmlrpc_curl_handle) {
                $curl = curl_init($method . '://' . $server . ':' . $port . $this->path);
                if ($keepAlive) {
                    $this->xmlrpc_curl_handle = $curl;
                }
            } else {
                $curl = $this->xmlrpc_curl_handle;
            }

            // results into variable
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

            if ($this->debug > 1) {
                curl_setopt($curl, CURLOPT_VERBOSE, true);
                /// @todo allow callers to redirect curlopt_stderr to some stream which can be buffered
            }
            curl_setopt($curl, CURLOPT_USERAGENT, $this->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($this->accepted_compression) && count($this->accepted_compression)) {
                //curl_setopt($curl, CURLOPT_ENCODING, implode(',', $this->accepted_compression));
                // empty string means 'any supported by CURL' (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
                if (count($this->accepted_compression) == 1) {
                    curl_setopt($curl, CURLOPT_ENCODING, $this->accepted_compression[0]);
                } else {
                    curl_setopt($curl, CURLOPT_ENCODING, '');
                }
            }
            // extra headers
            $headers = array('Content-Type: ' . $req->content_type, 'Accept-Charset: ' . implode(',', $this->accepted_charset_encodings));
            // if no keepalive is wanted, let the server know it in advance
            if (!$keepAlive) {
                $headers[] = 'Connection: close';
            }
            // request compression header
            if ($encodingHdr) {
                $headers[] = $encodingHdr;
            }

            curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
            // timeout is borked
            if ($timeout) {
                curl_setopt($curl, CURLOPT_TIMEOUT, $timeout == 1 ? 1 : $timeout - 1);
            }

            if ($username && $password) {
                curl_setopt($curl, CURLOPT_USERPWD, $username . ':' . $password);
                if (defined('CURLOPT_HTTPAUTH')) {
                    curl_setopt($curl, CURLOPT_HTTPAUTH, $authType);
                } elseif ($authType != 1) {
                    error_log('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth is supported by the current PHP/curl install');
                }
            }

            if ($method == 'https') {
                // set cert file
                if ($cert) {
                    curl_setopt($curl, CURLOPT_SSLCERT, $cert);
                }
                // set cert password
                if ($certPass) {
                    curl_setopt($curl, CURLOPT_SSLCERTPASSWD, $certPass);
                }
                // whether to verify remote host's cert
                curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verifypeer);
                // set ca certificates file/dir
                if ($caCert) {
                    curl_setopt($curl, CURLOPT_CAINFO, $caCert);
                }
                if ($caCertDir) {
                    curl_setopt($curl, CURLOPT_CAPATH, $caCertDir);
                }
                // set key file (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
                if ($key) {
                    curl_setopt($curl, CURLOPT_SSLKEY, $key);
                }
                // set key password (shall we catch errors in case CURLOPT_SSLKEY undefined ?)
                if ($keyPass) {
                    curl_setopt($curl, CURLOPT_SSLKEYPASSWD, $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, $this->verifyhost);
                // allow usage of different SSL versions
                curl_setopt($curl, CURLOPT_SSLVERSION, $sslVersion);
            }

            // proxy info
            if ($proxyHost) {
                if ($proxyPort == 0) {
                    $proxyPort = 8080; // NB: even for HTTPS, local connection is on port 8080
                }
                curl_setopt($curl, CURLOPT_PROXY, $proxyHost . ':' . $proxyPort);
                if ($proxyUsername) {
                    curl_setopt($curl, CURLOPT_PROXYUSERPWD, $proxyUsername . ':' . $proxyPassword);
                    if (defined('CURLOPT_PROXYAUTH')) {
                        curl_setopt($curl, CURLOPT_PROXYAUTH, $proxyAuthType);
                    } elseif ($proxyAuthType != 1) {
                        error_log('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth to proxy is supported by the current PHP/curl install');
                    }
                }
            }

            // NB: should we build cookie http headers by hand rather than let CURL do it?
            // the following code does not honour 'expires', 'path' and 'domain' cookie attributes
            // set to client obj the the user...
            if (count($this->cookies)) {
                $cookieHeader = '';
                foreach ($this->cookies as $name => $cookie) {
                    $cookieHeader .= $name . '=' . $cookie['value'] . '; ';
                }
                curl_setopt($curl, CURLOPT_COOKIE, substr($cookieHeader, 0, -2));
            }

            foreach ($this->extracurlopts as $opt => $val) {
                curl_setopt($curl, $opt, $val);
            }

            $result = curl_exec($curl);

            if ($this->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---";
                Logger::instance()->debugMessage($message);
            }

            if (!$result) {
                /// @todo we should use a better check here - what if we get back '' or '0'?

                $this->errstr = 'no response';
                $resp = new Response(0, PhpXmlRpc::$xmlrpcerr['curl_fail'], PhpXmlRpc::$xmlrpcstr['curl_fail'] . ': ' . curl_error($curl));
                curl_close($curl);
                if ($keepAlive) {
                    $this->xmlrpc_curl_handle = null;
                }
            } else {
                if (!$keepAlive) {
                    curl_close($curl);
                }
                $resp = $req->parseResponse($result, true, $this->return_type);
                // if we got back a 302, we can not reuse the curl handle for later calls
                if ($resp->faultCode() == PhpXmlRpc::$xmlrpcerr['http_error'] && $keepAlive) {
                    curl_close($curl);
                    $this->xmlrpc_curl_handle = null;
                }
            }

            return $resp;
        }

        /**
         * 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 xmlrpc 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 fourth 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 integer $timeout connection timeout (in seconds)
         * @param string $method the http protocol variant to be used
         * @param boolean fallback When true, upon receiving an error during multicall, multiple single calls will be attempted
         *
         * @return array
         */
        public function multicall($reqs, $timeout = 0, $method = '', $fallback = true)
        {
            if ($method == '') {
                $method = $this->method;
            }
            if (!$this->no_multicall) {
                $results = $this->_try_multicall($reqs, $timeout, $method);
                if (is_array($results)) {
                    // System.multicall succeeded
                    return $results;
                } else {
                    // either system.multicall is unsupported by server,
                    // or call failed for some other reason.
                    if ($fallback) {
                        // Don't try it next time...
                        $this->no_multicall = true;
                    } else {
                        if (is_a($results, '\PhpXmlRpc\Response')) {
                            $result = $results;
                        } else {
                            $result = new Response(0, PhpXmlRpc::$xmlrpcerr['multicall_error'], PhpXmlRpc::$xmlrpcstr['multicall_error']);
                        }
                    }
                }
            } 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
                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,
                // 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.
         * Returns either an array of xmlrpc reponses, an xmlrpc error response
         * or false (when received response does not respect valid multicall syntax).
         *
         * @param Request[] $reqs
         * @param int $timeout
         * @param string $method
         * @return array|bool|mixed|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 Request('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();

            if ($this->return_type == 'xml') {
                return $rets;
            } elseif ($this->return_type == 'phpvals') {
                /// @todo test this code branch...
                $rets = $result->value();
                if (!is_array($rets)) {
                    return false;       // bad return type from system.multicall
                }
                $numRets = count($rets);
                if ($numRets != count($reqs)) {
                    return false;       // wrong number of return values.
                }

                $response = array();
                for ($i = 0; $i < $numRets; $i++) {
                    $val = $rets[$i];
                    if (!is_array($val)) {
                        return false;
                    }
                    switch (count($val)) {
                        case 1:
                            if (!isset($val[0])) {
                                return false;       // Bad value
                            }
                            // Normal return value
                            $response[$i] = new Response($val[0], 0, '', 'phpvals');
                            break;
                        case 2:
                            /// @todo remove usage of @: it is apparently quite slow
                            $code = @$val['faultCode'];
                            if (!is_int($code)) {
                                return false;
                            }
                            $str = @$val['faultString'];
                            if (!is_string($str)) {
                                return false;
                            }
                            $response[$i] = new Response(0, $code, $str);
                            break;
                        default:
                            return false;
                    }
                }

                return $response;
            } else {
                // return type == 'xmlrpcvals'

                $rets = $result->value();
                if ($rets->kindOf() != 'array') {
                    return false;       // bad return type from system.multicall
                }
                $numRets = $rets->count();
                if ($numRets != count($reqs)) {
                    return false;       // wrong number of return values.
                }

                $response = array();
                foreach($rets as $val) {
                    switch ($val->kindOf()) {
                        case 'array':
                            if ($val->count() != 1) {
                                return false;       // Bad value
                            }
                            // Normal return value
                            $response[] = new Response($val[0]);
                            break;
                        case 'struct':
                            $code = $val['faultCode'];
                            if ($code->kindOf() != 'scalar' || $code->scalartyp() != 'int') {
                                return false;
                            }
                            $str = $val['faultString'];
                            if ($str->kindOf() != 'scalar' || $str->scalartyp() != 'string') {
                                return false;
                            }
                            $response[] = new Response(0, $code->scalarval(), $str->scalarval());
                            break;
                        default:
                            return false;
                    }
                }

                return $response;
            }
        }
    }







//use PhpXmlRpc\Helper\XMLParser;

    class Encoder
    {
        /**
         * Takes an xmlrpc value in object format and translates it into native PHP types.
         *
         * Works with xmlrpc requests objects as input, too.
         *
         * Given proper options parameter, can rebuild generic php object instances
         * (provided those have been encoded to xmlrpc 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 eanbling this!
         *
         * @author Dan Libby (dan@libby.com)
         *
         * @param Value|Request $xmlrpcVal
         * @param array $options if 'decode_php_objs' is set in the options array, xmlrpc structs can be decoded into php objects; if 'dates_as_objects' is set xmlrpc datetimes are decoded as php DateTime objects (standard is
         *
         * @return mixed
         */
        public function decode($xmlrpcVal, $options = array())
        {
            switch ($xmlrpcVal->kindOf()) {
                case 'scalar':
                    if (in_array('extension_api', $options)) {
                        reset($xmlrpcVal->me);
                        // list($typ, $val) = each($xmlrpcVal->me);
                        $typ = key($xmlrpcVal->me);
                        $val = current($xmlrpcVal->me);
                        switch ($typ) {
                            case 'dateTime.iso8601':
                                $xmlrpcVal->scalar = $val;
                                $xmlrpcVal->type = 'datetime';
                                $xmlrpcVal->timestamp = \PhpXmlRpc\Helper\Date::iso8601Decode($val);

                                return $xmlrpcVal;
                            case 'base64':
                                $xmlrpcVal->scalar = $val;
                                $xmlrpcVal->type = $typ;

                                return $xmlrpcVal;
                            default:
                                return $xmlrpcVal->scalarval();
                        }
                    }
                    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 xmlrpc value accepts safely strings, ints and datetimes,
                        // we cater to all 3 cases here
                        $out = $xmlrpcVal->scalarval();
                        if (is_string($out)) {
                            $out = strtotime($out);
                        }
                        if (is_int($out)) {
                            $result = new \Datetime();
                            $result->setTimestamp($out);

                            return $result;
                        } elseif (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 xmlrpc 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;
            }
        }

        /**
         * Takes native php types and encodes them into xmlrpc PHP object format.
         * It will not re-encode xmlrpc value objects.
         *
         * Feature creep -- could support more types via optional type argument
         * (string => datetime support has been added, ??? => base64 not yet)
         *
         * If given a proper options parameter, php object instances will be encoded
         * into 'special' xmlrpc values, that can later be decoded into php objects
         * by calling php_xmlrpc_decode() with a corresponding option
         *
         * @author Dan Libby (dan@libby.com)
         *
         * @param mixed $phpVal the value to be converted into an xmlrpc value object
         * @param array $options can include 'encode_php_objs', 'auto_dates', 'null_extension' or 'extension_api'
         *
         * @return \PhpXmlrpc\Value
         */
        public function encode($phpVal, $options = array())
        {
            $type = gettype($phpVal);
            switch ($type) {
                case 'string':
                    if (in_array('auto_dates', $options) && preg_match('/^[0-9]{8}T[0-9]{2}:[0-9]{2}:[0-9]{2}$/', $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;
                // <G_Giunta_2001-02-29>
                // Add support for encoding/decoding of booleans, since they are supported in PHP
                case 'boolean':
                    $xmlrpcVal = new Value($phpVal, Value::$xmlrpcBoolean);
                    break;
                // </G_Giunta_2001-02-29>
                case 'array':
                    // PHP arrays can be encoded to either xmlrpc structs or arrays,
                    // depending on wheter they are hashes or plain 0..n integer indexed
                    // 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;
                    } elseif (is_a($phpVal, 'DateTime')) {
                        $xmlrpcVal = new Value($phpVal->format('Ymd\TH:i:s'), Value::$xmlrpcStruct);
                    } else {
                        $arr = array();
                        reset($phpVal);
                        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 xmlrpc value:
                            // 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();
                    }
                // catch "user function", "unknown type"
                default:
                    // giancarlo pinerolo <ping@alt.it>
                    // it has to return
                    // an empty object in case, not a boolean.
                    $xmlrpcVal = new Value();
                    break;
            }

            return $xmlrpcVal;
        }

        /**
         * Convert the xml representation of a method response, method request or single
         * xmlrpc value into the appropriate object (a.k.a. deserialize).
         *
         * @param string $xmlVal
         * @param array $options
         *
         * @return mixed false on error, or an instance of either Value, Request or Response
         */
        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 prologue,
                // 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 ($valEncoding == 'ISO-8859-1') {
                        $xmlVal = utf8_encode($xmlVal);
                    } else {
                        if (extension_loaded('mbstring')) {
                            $xmlVal = mb_convert_encoding($xmlVal, 'UTF-8', $valEncoding);
                        } else {
                            error_log('XML-RPC: ' . __METHOD__ . ': invalid charset encoding of xml text: ' . $valEncoding);
                        }
                    }
                }
            }

            $parser = xml_parser_create();
            xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, true);
            // What if internal encoding is not in one of the 3 allowed?
            // we use the broadest one, ie. utf8!
            if (!in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
                xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
            } else {
                xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, PhpXmlRpc::$xmlrpc_internalencoding);
            }

            $xmlRpcParser = new XMLParser();
            xml_set_object($parser, $xmlRpcParser);

            xml_set_element_handler($parser, 'xmlrpc_se_any', 'xmlrpc_ee');
            xml_set_character_data_handler($parser, 'xmlrpc_cd');
            xml_set_default_handler($parser, 'xmlrpc_dh');
            if (!xml_parse($parser, $xmlVal, 1)) {
                $errstr = sprintf('XML error: %s at line %d, column %d',
                    xml_error_string(xml_get_error_code($parser)),
                    xml_get_current_line_number($parser), xml_get_current_column_number($parser));
                error_log($errstr);
                xml_parser_free($parser);

                return false;
            }
            xml_parser_free($parser);
            if ($xmlRpcParser->_xh['isf'] > 1) {
                // test that $xmlrpc->_xh['value'] is an obj, too???

                error_log($xmlRpcParser->_xh['isf_reason']);

                return false;
            }
            switch ($xmlRpcParser->_xh['rt']) {
                case 'methodresponse':
                    $v = &$xmlRpcParser->_xh['value'];
                    if ($xmlRpcParser->_xh['isf'] == 1) {
                        $vc = $v['faultCode'];
                        $vs = $v['faultString'];
                        $r = new Response(0, $vc->scalarval(), $vs->scalarval());
                    } else {
                        $r = new Response($v);
                    }

                    return $r;
                case 'methodcall':
                    $req = new Request($xmlRpcParser->_xh['method']);
                    for ($i = 0; $i < count($xmlRpcParser->_xh['params']); $i++) {
                        $req->addParam($xmlRpcParser->_xh['params'][$i]);
                    }

                    return $req;
                case 'value':
                    return $xmlRpcParser->_xh['value'];
                default:
                    return false;
            }
        }

    }







//use PhpXmlRpc\Helper\Charset;
//use PhpXmlRpc\Helper\Http;
//use PhpXmlRpc\Helper\Logger;
//use PhpXmlRpc\Helper\XMLParser;

    class Request
    {
        /// @todo: do these need to be public?
        public $payload;
        public $methodname;
        public $params = array();
        public $debug = 0;
        public $content_type = 'text/xml';

        // holds data while parsing the response. NB: Not a full Response object
        protected $httpResponse = array();

        /**
         * @param string $methodName the name of the method to invoke
         * @param Value[] $params array of parameters to be passed to the method (Value objects)
         */
        public function __construct($methodName, $params = array())
        {
            $this->methodname = $methodName;
            foreach ($params as $param) {
                $this->addParam($param);
            }
        }

        public function xml_header($charsetEncoding = '')
        {
            if ($charsetEncoding != '') {
                return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\" ?" . ">\n<methodCall>\n";
            } else {
                return "<?xml version=\"1.0\"?" . ">\n<methodCall>\n";
            }
        }

        public function xml_footer()
        {
            return '</methodCall>';
        }

        public function createPayload($charsetEncoding = '')
        {
            if ($charsetEncoding != '') {
                $this->content_type = 'text/xml; charset=' . $charsetEncoding;
            } else {
                $this->content_type = 'text/xml';
            }
            $this->payload = $this->xml_header($charsetEncoding);
            $this->payload .= '<methodName>' . Charset::instance()->encodeEntities($this->methodname, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "</methodName>\n";
            $this->payload .= "<params>\n";
            foreach ($this->params as $p) {
                $this->payload .= "<param>\n" . $p->serialize($charsetEncoding) .
                    "</param>\n";
            }
            $this->payload .= "</params>\n";
            $this->payload .= $this->xml_footer();
        }

        /**
         * Gets/sets the xmlrpc 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;
        }

        /**
         * Returns xml representation of the message. XML prologue included.
         *
         * @param string $charsetEncoding
         *
         * @return string the xml representation of the message, xml prologue included
         */
        public function serialize($charsetEncoding = '')
        {
            $this->createPayload($charsetEncoding);

            return $this->payload;
        }

        /**
         * Add a parameter to the list of parameters to be used upon method invocation.
         *
         * @param Value $param
         *
         * @return boolean false on failure
         */
        public function addParam($param)
        {
            // add check: do not add to self params which are not xmlrpc values
            if (is_object($param) && is_a($param, 'PhpXmlRpc\Value')) {
                $this->params[] = $param;

                return true;
            } else {
                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);
        }

        /**
         * Given an open file handle, read all data available and parse it as an xmlrpc 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
         *
         * @return Response
         *
         * @todo add 2nd & 3rd param to be passed to ParseResponse() ???
         */
        public function parseResponseFile($fp)
        {
            $ipd = '';
            while ($data = fread($fp, 32768)) {
                $ipd .= $data;
            }
            return $this->parseResponse($ipd);
        }

        /**
         * Parse the xmlrpc response contained in the string $data and return a Response object.
         *
         * @param string $data the xmlrpc response, eventually 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
         */
        public function parseResponse($data = '', $headersProcessed = false, $returnType = 'xmlrpcvals')
        {
            if ($this->debug) {
                Logger::instance()->debugMessage("---GOT---\n$data\n---END---");
            }

            $this->httpResponse = array('raw_data' => $data, 'headers' => array(), 'cookies' => array());

            if ($data == '') {
                error_log('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 {
                    $this->httpResponse = $httpParser->parseResponseHeaders($data, $headersProcessed, $this->debug);
                } catch(\Exception $e) {
                    $r = new Response(0, $e->getCode(), $e->getMessage());
                    // failed processing of HTTP response headers
                    // save into response obj the full payload received, for debugging
                    $r->raw_data = $data;

                    return $r;
                }
            }

            // be tolerant of extra whitespace in response body
            $data = trim($data);

            /// @todo 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 <luca.mariano@email.it> 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(@$this->httpResponse['headers']['content-type'], $data);

            if ($this->debug) {
                $start = strpos($data, '<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
                if ($start) {
                    $start += strlen('<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
                    $end = strpos($data, '-->', $start);
                    $comments = substr($data, $start, $end - $start);
                    Logger::instance()->debugMessage("---SERVER DEBUG INFO (DECODED) ---\n\t" .
                        str_replace("\n", "\n\t", base64_decode($comments)) . "\n---END---", $respEncoding);
                }
            }

            // if user wants back raw xml, give it to him
            if ($returnType == 'xml') {
                $r = new Response($data, 0, '', 'xml');
                $r->hdrs = $this->httpResponse['headers'];
                $r->_cookies = $this->httpResponse['cookies'];
                $r->raw_data = $this->httpResponse['raw_data'];

                return $r;
            }

            if ($respEncoding != '') {

                // Since parsing will fail if charset is not specified in the xml prologue,
                // 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 ($respEncoding == 'ISO-8859-1') {
                        $data = utf8_encode($data);
                    } else {
                        if (extension_loaded('mbstring')) {
                            $data = mb_convert_encoding($data, 'UTF-8', $respEncoding);
                        } else {
                            error_log('XML-RPC: ' . __METHOD__ . ': invalid charset encoding of received response: ' . $respEncoding);
                        }
                    }
                }
            }

            $parser = xml_parser_create();
            xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, true);
            // G. Giunta 2005/02/13: PHP internally uses 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, ie. utf8
            // This allows to send data which is native in various charset,
            // by extending xmlrpc_encode_entities() and setting xmlrpc_internalencoding
            if (!in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
                xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
            } else {
                xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, PhpXmlRpc::$xmlrpc_internalencoding);
            }

            $xmlRpcParser = new XMLParser();
            xml_set_object($parser, $xmlRpcParser);

            if ($returnType == 'phpvals') {
                xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast');
            } else {
                xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee');
            }

            xml_set_character_data_handler($parser, 'xmlrpc_cd');
            xml_set_default_handler($parser, 'xmlrpc_dh');

            // first error check: xml not well formed
            if (!xml_parse($parser, $data, strlen($data))) {
                // thanks to Peter Kocks <peter.kocks@baygate.com>
                if ((xml_get_current_line_number($parser)) == 1) {
                    $errStr = 'XML error at line 1, check URL';
                } else {
                    $errStr = sprintf('XML error: %s at line %d, column %d',
                        xml_error_string(xml_get_error_code($parser)),
                        xml_get_current_line_number($parser), xml_get_current_column_number($parser));
                }
                error_log($errStr);
                $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'], PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' (' . $errStr . ')');
                xml_parser_free($parser);
                if ($this->debug) {
                    print $errStr;
                }
                $r->hdrs = $this->httpResponse['headers'];
                $r->_cookies = $this->httpResponse['cookies'];
                $r->raw_data = $this->httpResponse['raw_data'];

                return $r;
            }
            xml_parser_free($parser);
            // second error check: xml well formed but not xml-rpc compliant
            if ($xmlRpcParser->_xh['isf'] > 1) {
                if ($this->debug) {
                    /// @todo echo something for user?
                }

                $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'],
                    PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' ' . $xmlRpcParser->_xh['isf_reason']);
            }
            // third error check: parsing of the response has somehow gone boink.
            // NB: shall we omit this check, since we trust the parsing code?
            elseif ($returnType == 'xmlrpcvals' && !is_object($xmlRpcParser->_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['invalid_return'],
                    PhpXmlRpc::$xmlrpcstr['invalid_return']);
            } else {
                if ($this->debug > 1) {
                    Logger::instance()->debugMessage(
                        "---PARSED---\n".var_export($xmlRpcParser->_xh['value'], true)."\n---END---"
                    );
                }

                // note that using =& will raise an error if $xmlRpcParser->_xh['st'] does not generate an object.
                $v = &$xmlRpcParser->_xh['value'];

                if ($xmlRpcParser->_xh['isf']) {
                    /// @todo we should test here if server sent an int and a string, and/or coerce them into such...
                    if ($returnType == '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
                        $errNo = -1;
                    }

                    $r = new Response(0, $errNo, $errStr);
                } else {
                    $r = new Response($v, 0, '', $returnType);
                }
            }

            $r->hdrs = $this->httpResponse['headers'];
            $r->_cookies = $this->httpResponse['cookies'];
            $r->raw_data = $this->httpResponse['raw_data'];

            return $r;
        }

        /**
         * Kept the old name even if Request class was renamed, for compatibility.
         *
         * @return string
         */
        public function kindOf()
        {
            return 'msg';
        }

        /**
         * Enables/disables the echoing to screen of the xmlrpc responses received.
         *
         * @param integer $in values 0, 1, 2 are supported
         */
        public function setDebug($in)
        {
            $this->debug = $in;
        }
    }








//use PhpXmlRpc\Helper\Charset;

    class Response
    {
        /// @todo: do these need to be public?
        public $val = 0;
        public $valType;
        public $errno = 0;
        public $errstr = '';
        public $payload;
        public $hdrs = array();
        public $_cookies = array();
        public $content_type = 'text/xml';
        public $raw_data = '';

        /**
         * @param mixed $val either an xmlrpc value obj, a php value or the xml serialization of an xmlrpc value (a string)
         * @param integer $fCode set it to anything but 0 to create an error response
         * @param string $fString the error string, in case of an error response
         * @param string $valType either 'xmlrpcvals', 'phpvals' or 'xml'
         *
         * @todo add check that $val / $fCode / $fString is of correct type???
         * NB: as of now we do not do it, since it might be either an xmlrpc value or a plain
         * php val, or a complete xml chunk, depending on usage of Client::send() inside which creator is called...
         */
        public function __construct($val, $fCode = 0, $fString = '', $valType = '')
        {
            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 {
                    // user declares type of resp value: believe him
                    $this->valtyp = $valType;
                }
            }
        }

        /**
         * 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.
         *
         * @return Value|string|mixed the xmlrpc value object returned by the server. Might be an xml string or php value if the response has been created by specially configured Client objects
         */
        public function value()
        {
            return $this->val;
        }

        /**
         * 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.
         *
         * @return array array of cookies received from the server
         */
        public function cookies()
        {
            return $this->_cookies;
        }

        /**
         * Returns xml representation of the response. XML prologue not included.
         *
         * @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 \Exception
         */
        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) {
                // G. Giunta 2005/2/13: 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>" .
                    Charset::instance()->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')) {
                    if (is_string($this->val) && $this->valtyp == 'xml') {
                        $result .= "<params>\n<param>\n" .
                            $this->val .
                            "</param>\n</params>";
                    } else {
                        /// @todo try to build something serializable?
                        throw new \Exception('cannot serialize xmlrpc response objects whose content is native php values');
                    }
                } else {
                    $result .= "<params>\n<param>\n" .
                        $this->val->serialize($charsetEncoding) .
                        "</param>\n</params>";
                }
            }
            $result .= "\n</methodResponse>";
            $this->payload = $result;

            return $result;
        }
    }






//use PhpXmlRpc\Helper\XMLParser;
//use PhpXmlRpc\Helper\Charset;

    class Server
    {
        /**
         * Array defining php functions exposed as xmlrpc methods by this server.
         */
        protected $dmap = array();
        /**
         * Defines how functions in dmap will be invoked: either using an xmlrpc request object
         * or plain php values.
         * Valid strings are 'xmlrpcvals', 'phpvals' or 'epivals'
         */
        public $functions_parameters_type = 'xmlrpcvals';
        /**
         * Option used for fine-tuning the encoding the php values returned from
         * functions registered in the dispatch map when the functions_parameters_types
         * member is set to 'phpvals'
         * @see Encoder::encode for a list of values
         */
        public $phpvals_encoding_options = array('auto_dates');
        /**
         * Controls whether the server is going to echo debugging messages back to the client as comments in response body.
         * Valid values: 0,1,2,3
         */
        public $debug = 1;
        /**
         * Controls behaviour of server when the invoked user function throws an exception:
         * 0 = catch it and return an 'internal error' xmlrpc response (default)
         * 1 = catch it and return an xmlrpc response with the error corresponding to the exception
         * 2 = allow the exception to float to the upper layers
         */
        public $exception_handling = 0;
        /**
         * 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.
         * Set at constructor time.
         */
        public $compress_response = false;
        /**
         * List of http compression methods accepted by the server for requests. Set at constructor time.
         * NB: PHP supports deflate, gzip compressions out of the box if compiled w. zlib
         */
        public $accepted_compression = array();
        /// shall we serve calls to system.* methods?
        public $allow_system_funcs = true;
        /**
         * List of charset encodings natively accepted for requests.
         * Set at constructor time.
         * UNUSED so far...
         */
        public $accepted_charset_encodings = array();
        /**
         * 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 at present, unless mbstring is enabled),
         * null (leave unspecified in response, convert output stream to US_ASCII),
         * 'default' (use xmlrpc library default as specified in xmlrpc.inc, convert output stream if needed),
         * or '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)
         */
        public $response_charset_encoding = '';
        /**
         * Storage for internal debug info.
         */
        protected $debug_info = '';
        /**
         * Extra data passed at runtime to method handling functions. Used only by EPI layer
         */
        public $user_data = null;

        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
         * @param boolean $serviceNow set to false 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 = array('gzip', 'deflate');
                $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 class add_to_map() function
            * to add functions manually (borrowed from SOAPX4)
            */
            if ($dispatchMap) {
                $this->dmap = $dispatchMap;
                if ($serviceNow) {
                    $this->service();
                }
            }
        }

        /**
         * Set debug level of server.
         *
         * @param integer $level debug lvl: determines info added to xmlrpc responses (as xml comments)
         *                    0 = no debug info,
         *                    1 = msgs set from user with debugmsg(),
         *                    2 = add complete xmlrpc 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 an USER_ERROR level error will not halt script
         *                    execution anymore, but just end up logged in the xmlrpc response)
         *                    Note that info added at level 2 and 3 will be base64 encoded
         */
        public function setDebug($level)
        {
            $this->debug = $level;
        }

        /**
         * 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
         * @access public
         */
        public static function xmlrpc_debugmsg($msg)
        {
            static::$_xmlrpc_debuginfo .= $msg . "\n";
        }

        public static function error_occurred($msg)
        {
            static::$_xmlrpcs_occurred_errors .= $msg . "\n";
        }

        /**
         * Return a string with the serialized representation of all debug info.
         *
         * @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" . Charset::instance()->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 xmlrpc 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+++");
            }

            $r = $this->parseRequestHeaders($data, $reqCharset, $respCharset, $respEncoding);
            if (!$r) {
                // this actually executes the request
                $r = $this->parseRequest($data, $reqCharset);
            }

            // save full body of request into response, for more debugging usages
            $r->raw_data = $rawData;

            if ($this->debug > 2 && static::$_xmlrpcs_occurred_errors) {
                $this->debugmsg("+++PROCESSING ERRORS AND WARNINGS+++\n" .
                    static::$_xmlrpcs_occurred_errors . "+++END+++");
            }

            $payload = $this->xml_header($respCharset);
            if ($this->debug > 0) {
                $payload = $payload . $this->serializeDebug($respCharset);
            }

            // G. Giunta 2006-01-27: do not create response serialization if it has
            // already happened. Helps building json magic
            if (empty($r->payload)) {
                $r->serialize($respCharset);
            }
            $payload = $payload . $r->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: ' . $r->content_type);
                // we do not know if client actually told us an accepted charset, but if he did
                // we have to tell him 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 && function_exists('gzencode') && $respEncoding != ''
                    && $phpNoSelfCompress
                ) {
                    if (strpos($respEncoding, 'gzip') !== false) {
                        $payload = gzencode($payload);
                        header("Content-Encoding: gzip");
                        header("Vary: Accept-Encoding");
                    } elseif (strpos($respEncoding, 'deflate') !== false) {
                        $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
                if ($phpNoSelfCompress) {
                    header('Content-Length: ' . (int)strlen($payload));
                }
            } else {
                error_log('XML-RPC: ' . __METHOD__ . ': http headers already sent before response is fully generated. Check for php warning or error messages');
            }

            print $payload;

            // return request, in case subclasses want it
            return $r;
        }

        /**
         * Add a method to the dispatch map.
         *
         * @param string $methodName the name with which the method will be made available
         * @param string $function the php function that will get invoked
         * @param array $sig the array of valid method signatures
         * @param string $doc method documentation
         * @param array $sigDoc the array of valid method signatures docs (one string per param, one for return type)
         */
        public function add_to_map($methodName, $function, $sig = null, $doc = false, $sigDoc = false)
        {
            $this->dmap[$methodName] = array(
                'function' => $function,
                'docstring' => $doc,
            );
            if ($sig) {
                $this->dmap[$methodName]['signature'] = $sig;
            }
            if ($sigDoc) {
                $this->dmap[$methodName]['signature_docs'] = $sigDoc;
            }
        }

        /**
         * Verify type and number of parameters received against a list of known signatures.
         *
         * @param array|Request $in array of either xmlrpc value objects or xmlrpc type definitions
         * @param array $sigs array of known signatures to match against
         *
         * @return array
         */
        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 xmlrpc request. If needed, inflate request.
         *
         * @return mixed Response|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) {
                error_log('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 = '';
            }

            // 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
                    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 Response(0, PhpXmlRpc::$xmlrpcerr['server_decompress_fail'], PhpXmlRpc::$xmlrpcstr['server_decompress_fail']);

                            return $r;
                        }
                    } else {
                        $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_cannot_decompress'], PhpXmlRpc::$xmlrpcstr['server_cannot_decompress']);

                        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 should check if we can match the client-requested encoding
                    // with the encodings we know we can generate.
                    /// @todo we should parse q=0.x preferences instead of getting first charset specified...
                    $clientAcceptedCharsets = explode(',', strtoupper($_SERVER['HTTP_ACCEPT_CHARSET']));
                    // Give preference to internal encoding
                    $knownCharsets = array(PhpXmlRpc::$xmlrpc_internalencoding, 'UTF-8', 'ISO-8859-1', 'US-ASCII');
                    foreach ($knownCharsets as $charset) {
                        foreach ($clientAcceptedCharsets as $accepted) {
                            if (strpos($accepted, $charset) === 0) {
                                $respEncoding = $charset;
                                break;
                            }
                        }
                        if ($respEncoding) {
                            break;
                        }
                    }
                }
            } 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???
            $reqEncoding = XMLParser::guessEncoding(isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : '',
                $data);

            return;
        }

        /**
         * Parse an xml chunk containing an xmlrpc request and execute the corresponding
         * php function registered with the server.
         *
         * @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)
         */
        public function parseRequest($data, $reqEncoding = '')
        {
            // decompose incoming XML into request structure

            if ($reqEncoding != '') {
                // Since parsing will fail if charset is not specified in the xml prologue,
                // 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($reqEncoding, array('UTF-8')))
                if (!in_array($reqEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) {
                    if ($reqEncoding == 'ISO-8859-1') {
                        $data = utf8_encode($data);
                    } else {
                        if (extension_loaded('mbstring')) {
                            $data = mb_convert_encoding($data, 'UTF-8', $reqEncoding);
                        } else {
                            error_log('XML-RPC: ' . __METHOD__ . ': invalid charset encoding of received request: ' . $reqEncoding);
                        }
                    }
                }
            }

            $parser = xml_parser_create();
            xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, true);
            // G. Giunta 2005/02/13: PHP internally uses 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, ie. utf8
            // This allows to send data which is native in various charset,
            // by extending xmlrpc_encode_entities() and setting xmlrpc_internalencoding
            if (!in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
                xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
            } else {
                xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, PhpXmlRpc::$xmlrpc_internalencoding);
            }

            $xmlRpcParser = new XMLParser();
            xml_set_object($parser, $xmlRpcParser);

            if ($this->functions_parameters_type != 'xmlrpcvals') {
                xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast');
            } else {
                xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee');
            }
            xml_set_character_data_handler($parser, 'xmlrpc_cd');
            xml_set_default_handler($parser, 'xmlrpc_dh');
            if (!xml_parse($parser, $data, 1)) {
                // return XML error as a faultCode
                $r = new Response(0,
                    PhpXmlRpc::$xmlrpcerrxml + xml_get_error_code($parser),
                    sprintf('XML error: %s at line %d, column %d',
                        xml_error_string(xml_get_error_code($parser)),
                        xml_get_current_line_number($parser), xml_get_current_column_number($parser)));
                xml_parser_free($parser);
            } elseif ($xmlRpcParser->_xh['isf']) {
                xml_parser_free($parser);
                $r = new Response(0,
                    PhpXmlRpc::$xmlrpcerr['invalid_request'],
                    PhpXmlRpc::$xmlrpcstr['invalid_request'] . ' ' . $xmlRpcParser->_xh['isf_reason']);
            } else {
                xml_parser_free($parser);
                // small layering violation in favor of speed and memory usage:
                // we should allow the 'execute' method handle this, but in the
                // most common scenario (xmlrpc 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[$xmlRpcParser->_xh['method']]['parameters_type']) && ($this->dmap[$xmlRpcParser->_xh['method']]['parameters_type'] == 'phpvals'))) {
                    if ($this->debug > 1) {
                        $this->debugmsg("\n+++PARSED+++\n" . var_export($xmlRpcParser->_xh['params'], true) . "\n+++END+++");
                    }
                    $r = $this->execute($xmlRpcParser->_xh['method'], $xmlRpcParser->_xh['params'], $xmlRpcParser->_xh['pt']);
                } else {
                    // build a Request object with data parsed from xml
                    $req = new Request($xmlRpcParser->_xh['method']);
                    // now add parameters in
                    for ($i = 0; $i < count($xmlRpcParser->_xh['params']); $i++) {
                        $req->addParam($xmlRpcParser->_xh['params'][$i]);
                    }

                    if ($this->debug > 1) {
                        $this->debugmsg("\n+++PARSED+++\n" . var_export($req, true) . "\n+++END+++");
                    }
                    $r = $this->execute($req);
                }
            }

            return $r;
        }

        /**
         * Execute a method invoked by the client, checking parameters used.
         *
         * @param mixed $req either a Request obj or a method name
         * @param array $params array with method parameters as php types (if m is method name only)
         * @param array $paramTypes array with xmlrpc types of method parameters (if m is method name only)
         *
         * @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)) {
                $methName = $req->method();
            } else {
                $methName = $req;
            }
            $sysCall = $this->allow_system_funcs && (strpos($methName, "system.") === 0);
            $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;

            if (!isset($dmap[$methName]['function'])) {
                // No such method
                return new Response(0,
                    PhpXmlRpc::$xmlrpcerr['unknown_method'],
                    PhpXmlRpc::$xmlrpcstr['unknown_method']);
            }

            // Check signature
            if (isset($dmap[$methName]['signature'])) {
                $sig = $dmap[$methName]['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 Response(
                        0,
                        PhpXmlRpc::$xmlrpcerr['incorrect_params'],
                        PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": ${errStr}"
                    );
                }
            }

            $func = $dmap[$methName]['function'];
            // let the 'class::function' syntax be accepted in dispatch maps
            if (is_string($func) && strpos($func, '::')) {
                $func = explode('::', $func);
            }

            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)) {
                error_log("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler is not callable");
                return new Response(
                    0,
                    PhpXmlRpc::$xmlrpcerr['server_error'],
                    PhpXmlRpc::$xmlrpcstr['server_error'] . ": no function matches method"
                );
            }

            // 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)) {
                    if ($sysCall) {
                        $r = call_user_func($func, $this, $req);
                    } else {
                        $r = call_user_func($func, $req);
                    }
                    if (!is_a($r, 'PhpXmlRpc\Response')) {
                        error_log("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 Response($r);
                        } else {
                            $r = new Response(
                                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($methName, $params, $this->user_data));
                            // mimic EPI behaviour: if we get an array that looks like an error, make it
                            // an eror response
                            if (is_array($r) && array_key_exists('faultCode', $r) && array_key_exists('faultString', $r)) {
                                $r = new Response(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
                                $r = new Response(php_xmlrpc_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')) {
                        // what should we assume here about automatic encoding of datetimes
                        // and php classes instances???
                        $r = new Response(php_xmlrpc_encode($r, $this->phpvals_encoding_options));
                    }
                }
            } catch (\Exception $e) {
                // (barring errors in the lib) an uncatched exception happened
                // in the called function, we wrap it in a proper error-response
                switch ($this->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;
                        break;
                    case 1:
                        $r = new Response(0, $e->getCode(), $e->getMessage());
                        break;
                    default:
                        $r = new Response(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;
        }

        /**
         * add a string to the 'internal debug message' (separate from 'user debug message').
         *
         * @param string $string
         */
        protected function debugmsg($string)
        {
            $this->debug_info .= $string . "\n";
        }

        /**
         * @param string $charsetEncoding
         * @return string
         */
        protected function xml_header($charsetEncoding = '')
        {
            if ($charsetEncoding != '') {
                return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\"?" . ">\n";
            } else {
                return "<?xml version=\"1.0\"?" . ">\n";
            }
        }

        /* Functions that implement system.XXX methods of xmlrpc servers */

        /**
         * @return array
         */
        public function getSystemDispatchMap()
        {
            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 capabilites 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(
                // xmlrpc spec: always supported
                'xmlrpc' => array(
                    'specUrl' => 'http://www.xmlrpc.com/spec',
                    'specVersion' => 1
                ),
                // if we support system.xxx functions, we always support multicall, too...
                // Note that, as of 2006/09/17, the following URL does not respond anymore
                'system.multicall' => array(
                    'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208',
                    'specVersion' => 1
                ),
                // introspection: version 2! we support 'mixed', too
                'introspection' => array(
                    'specUrl' => 'http://phpxmlrpc.sourceforge.net/doc-2/ch10.html',
                    'specVersion' => 2,
                ),
            );

            // NIL extension
            if (PhpXmlRpc::$xmlrpc_null_extension) {
                $outAr['nil'] = array(
                    'specUrl' => 'http://www.ontosys.com/xml-rpc/extensions.php',
                    'specVersion' => 1
                );
            }

            return $outAr;
        }

        public static function _xmlrpcs_getCapabilities($server, $req = null)
        {
            $encoder = new Encoder();
            return new Response($encoder->encode($server->getCapabilities()));
        }

        public static function _xmlrpcs_listMethods($server, $req = null) // if called in plain php values mode, second param is missing
        {
            $outAr = array();
            foreach ($server->dmap as $key => $val) {
                $outAr[] = new Value($key, 'string');
            }
            if ($server->allow_system_funcs) {
                foreach ($server->getSystemDispatchMap() as $key => $val) {
                    $outAr[] = new Value($key, 'string');
                }
            }

            return new Response(new Value($outAr, 'array'));
        }

        public static function _xmlrpcs_methodSignature($server, $req)
        {
            // let accept as parameter both an xmlrpc value or string
            if (is_object($req)) {
                $methName = $req->getParam(0);
                $methName = $methName->scalarval();
            } else {
                $methName = $req;
            }
            if (strpos($methName, "system.") === 0) {
                $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 Response(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 Response(new Value('undef', 'string'));
                }
            } else {
                $r = new Response(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
            }

            return $r;
        }

        public static function _xmlrpcs_methodHelp($server, $req)
        {
            // let accept as parameter both an xmlrpc value or string
            if (is_object($req)) {
                $methName = $req->getParam(0);
                $methName = $methName->scalarval();
            } else {
                $methName = $req;
            }
            if (strpos($methName, "system.") === 0) {
                $dmap = $server->getSystemDispatchMap();
            } else {
                $dmap = $server->dmap;
            }
            if (isset($dmap[$methName])) {
                if (isset($dmap[$methName]['docstring'])) {
                    $r = new Response(new Value($dmap[$methName]['docstring']), 'string');
                } else {
                    $r = new Response(new Value('', 'string'));
                }
            } else {
                $r = new Response(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
            }

            return $r;
        }

        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');
        }

        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 Response(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');
        }

        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 real dirty and simplistic hack, since we might have received a
            // base64 or datetime values, but they will be listed as strings here...
            $numParams = count($call['params']);
            $pt = array();
            $wrapper = new Wrapper();
            foreach ($call['params'] as $val) {
                $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');
        }

        public static function _xmlrpcs_multicall($server, $req)
        {
            $result = array();
            // let accept a plain list of php parameters, beside a single xmlrpc 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 Response(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 xmlrpc 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.
         */
        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) {
                \PhpXmlRpc\Server::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)) {
                    error_log($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);
                    }
                }
            }
        }
    }





    //use PhpXmlRpc\Helper\Charset;

    class Value implements \Countable, \IteratorAggregate, \ArrayAccess
    {
        public static $xmlrpcI4 = "i4";
        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,
            "int" => 1,
            "boolean" => 1,
            "double" => 1,
            "string" => 1,
            "dateTime.iso8601" => 1,
            "base64" => 1,
            "array" => 2,
            "struct" => 3,
            "null" => 1,
        );

        /// @todo: do these need to be public?
        public $me = array();
        public $mytype = 0;
        public $_php_class = null;

        /**
         * Build an xmlrpc value.
         * When no value or type is passed in, the value is left uninitialized, and the value can be added later
         *
         * @param mixed $val
         * @param string $type any valid xmlrpc type name (lowercase). If null, 'string' is assumed
         */
        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 '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:
                        error_log("XML-RPC: " . __METHOD__ . ": not a known type ($type)");
                }
            }
        }

        /**
         * Add a single php value to an (uninitialized) xmlrpc value.
         *
         * @param mixed $val
         * @param string $type
         *
         * @return int 1 or 0 on failure
         */
        public function addScalar($val, $type = 'string')
        {
            $typeOf = null;
            if (isset(static::$xmlrpcTypes[$type])) {
                $typeOf = static::$xmlrpcTypes[$type];
            }

            if ($typeOf !== 1) {
                error_log("XML-RPC: " . __METHOD__ . ": not a scalar type ($type)");
                return 0;
            }

            // coerce booleans into correct values
            // NB: we should either do it for datetimes, integers 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:
                    error_log('XML-RPC: ' . __METHOD__ . ': scalar xmlrpc value can have only one value');
                    return 0;
                case 3:
                    error_log('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
                    $this->me['array'][] = new Value($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 xmlrpc values objects to an xmlrpc value.
         *
         * @param Value[] $values
         *
         * @return int 1 or 0 on failure
         *
         * @todo add some checking for $values to be an array of xmlrpc values?
         */
        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 {
                error_log('XML-RPC: ' . __METHOD__ . ': already initialized as a [' . $this->kindOf() . ']');
                return 0;
            }
        }

        /**
         * Add an array of named xmlrpc value objects to an xmlrpc value.
         *
         * @param Value[] $values
         *
         * @return int 1 or 0 on failure
         *
         * @todo add some checking for $values to be an array?
         */
        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 {
                error_log('XML-RPC: ' . __METHOD__ . ': already initialized as a [' . $this->kindOf() . ']');
                return 0;
            }
        }

        /**
         * Returns a string containing "struct", "array", "scalar" or "undef" describing the base type of the value.
         *
         * @return string
         */
        public function kindOf()
        {
            switch ($this->mytype) {
                case 3:
                    return 'struct';
                    break;
                case 2:
                    return 'array';
                    break;
                case 1:
                    return 'scalar';
                    break;
                default:
                    return 'undef';
            }
        }

        protected function serializedata($typ, $val, $charsetEncoding = '')
        {
            $rs = '';

            if (!isset(static::$xmlrpcTypes[$typ])) {
                return $rs;
            }

            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:
                            // G. Giunta 2005/2/13: do NOT use htmlentities, since
                            // it will produce named html entities, which are invalid xml
                            $rs .= "<${typ}>" . Charset::instance()->encodeEntities($val, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "</${typ}>";
                            break;
                        case static::$xmlrpcInt:
                        case static::$xmlrpcI4:
                            $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 xmlrpc spec forbids exponential notation.
                            // sprintf('%F') could be most likely ok but it fails eg. 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, 128, '.', '')) . "</${typ}>";
                            break;
                        case static::$xmlrpcDateTime:
                            if (is_string($val)) {
                                $rs .= "<${typ}>${val}</${typ}>";
                            } elseif (is_a($val, 'DateTime')) {
                                $rs .= "<${typ}>" . $val->format('Ymd\TH:i:s') . "</${typ}>";
                            } elseif (is_int($val)) {
                                $rs .= "<${typ}>" . strftime("%Y%m%dT%H:%M:%S", $val) . "</${typ}>";
                            } else {
                                // not really a good idea here: but what shall 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 xmlrpc 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 = Charset::instance();
                    foreach ($val as $key2 => $val2) {
                        $rs .= '<member><name>' . $charsetEncoder->encodeEntities($key2, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "</name>\n";
                        //$rs.=$this->serializeval($val2);
                        $rs .= $val2->serialize($charsetEncoding);
                        $rs .= "</member>\n";
                    }
                    $rs .= '</struct>';
                    break;
                case 2:
                    // array
                    $rs .= "<array>\n<data>\n";
                    foreach ($val as $element) {
                        //$rs.=$this->serializeval($val[$i]);
                        $rs .= $element->serialize($charsetEncoding);
                    }
                    $rs .= "</data>\n</array>";
                    break;
                default:
                    break;
            }

            return $rs;
        }

        /**
         * Returns 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 = '')
        {
            // add check? slower, but helps to avoid recursion in serializing broken xmlrpc values...
            //if (is_object($o) && (get_class($o) == 'xmlrpcval' || is_subclass_of($o, 'xmlrpcval')))
            //{
            reset($this->me);

            // list($typ, $val) = each($this->me);
            $typ = key($this->me);
            $val = current($this->me);

            return '<value>' . $this->serializedata($typ, $val, $charsetEncoding) . "</value>\n";
            //}
        }

        /**
         * Checks whether a struct member with a given name is present.
         * Works only on xmlrpc 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)
        {
            return array_key_exists($key, $this->me['struct']);
        }

        /**
         * Returns the value of a given struct member (an xmlrpc 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)
        {
            return $this->me['struct'][$key];
        }

        /**
         * Reset internal pointer for xmlrpc values of type struct.
         * @deprecated iterate directly over the object using foreach instead
         */
        public function structreset()
        {
            reset($this->me['struct']);
        }

        /**
         * Return next member element for xmlrpc values of type struct.
         *
         * @return Value
         *
         * @deprecated iterate directly over the object using foreach instead
         */
        public function structeach()
        {
            return each($this->me['struct']);
        }

        /**
         * Returns the value of a scalar xmlrpc value.
         *
         * @return mixed
         */
        public function scalarval()
        {
            reset($this->me);
            // list(, $b) = each($this->me);
            $toss = key($this->me);
            $b = current($this->me);

            return $b;
        }

        /**
         * Returns the type of the xmlrpc value.
         * For integers, 'int' is always returned in place of 'i4'.
         *
         * @return string
         */
        public function scalartyp()
        {
            reset($this->me);
            // list($a,) = each($this->me);
            $a = key($this->me);
            $toss = current($this->me);
            if ($a == static::$xmlrpcI4) {
                $a = static::$xmlrpcInt;
            }

            return $a;
        }

        /**
         * Returns the m-th member of an xmlrpc value of struct 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)
        {
            return $this->me['array'][$key];
        }

        /**
         * Returns the number of members in an xmlrpc value of array type.
         *
         * @return integer
         *
         * @deprecated use count() instead
         */
        public function arraysize()
        {
            return count($this->me['array']);
        }

        /**
         * Returns the number of members in an xmlrpc value of struct type.
         *
         * @return integer
         *
         * @deprecated use count() instead
         */
        public function structsize()
        {
            return count($this->me['struct']);
        }

        /**
         * Returns the number of members in an xmlrpc value:
         * - 0 for uninitialized values
         * - 1 for scalars
         * - the number of elements for structs and arrays
         *
         * @return integer
         */
        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
         *
         * @return ArrayIterator
         */
        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();
            }
            return new \ArrayIterator();
        }


        public function offsetSet($offset, $value) {

            switch ($this->mytype) {
                case 3:
                    if (!($value instanceof \PhpXmlRpc\Value)) {
                        throw new \Exception('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 \Exception('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 \PhpXmlRpc\Value)) {
                        throw new \Exception('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: handle i4 vs int
                    reset($this->me);
                    // list($type,) = each($this->me);
                    $type = key($this->me);
                    $toss = current($this->me);
                    if ($type != $offset) {
                        throw new \Exception('');
                    }
                    $this->me[$type] = $value;
                    return;
                default:
                    // it would be nice to allow empty values to be be turned into non-empty ones this way, but we miss info to do so
                    throw new \Exception("XML-RPC Value is of type 'undef' and its value can not be set using array index");
            }
        }

        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:
    // todo: handle i4 vs int
                    return $offset == $this->scalartyp();
                default:
                    return false;
            }
        }

        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
                    throw new \Exception("XML-RPC Value is of type 'scalar' and its value can not be unset using array index");
                default:
                    throw new \Exception("XML-RPC Value is of type 'undef' and its value can not be unset using array index");
            }
        }

        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:
    // on bad type: null or exception?
                    reset($this->me);
                    // list($type, $value) = each($this->me);
                    $type = key($this->me);
                    $value = current($this->me);
                    return $type == $offset ? $value : null;
                default:
    // return null or exception?
                    throw new \Exception("XML-RPC Value is of type 'undef' and can not be accessed using array index");
            }
        }
    }





    /**
     * PHP-XMLRPC "wrapper" class.
     * Generate stubs to transparently access xmlrpc 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 when wrapping methods without obj rebuilding, use return_type = 'phpvals' (faster)
     */
    class Wrapper
    {
        /// used to hold a reference to object instances whose methods get wrapped by wrapPhpFunction(), in 'create source' mode
        public static $objHolder = array();

        /**
         * 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 xmlrpc structs
         * - for php arrays always return array, even though arrays sometimes serialize as json structs
         * - for 'void' and 'null' returns 'undefined'
         *
         * @param string $phpType
         *
         * @return string
         */
        public function php2XmlrpcType($phpType)
        {
            switch (strtolower($phpType)) {
                case 'string':
                    return Value::$xmlrpcString;
                case 'integer':
                case Value::$xmlrpcInt: // 'int'
                case Value::$xmlrpcI4:
                    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':
                    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)) {
                        return Value::$xmlrpcStruct;
                    } else {
                        // unknown: might be any 'extended' xmlrpc 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':
                    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 xmlrpc type
                    return strtolower($xmlrpcType);
            }
        }

        /**
         * Given a user-defined PHP function, create a PHP 'wrapper' function that can
         * be exposed as xmlrpc method from an xmlrpc 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 xmlrpc 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 vals 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 xmlrpc responses:
         *   when the xmlrpc 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 (ie. functions not expecting a single Request obj as parameter)
         * is by making use of the functions_parameters_type class member.
         *
         * @param callable $callable the PHP user function to be exposed as xmlrpc 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_php_objs   let php objects be sent to server using the 'improved' xmlrpc 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 trigger_errors / error_log 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]))) {
                    error_log('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) {
                    error_log('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) {
                error_log('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()) {
                    error_log('XML-RPC: ' . __METHOD__ . ': method to be wrapped is private: ' . $plainFuncName);
                    return false;
                }
                if ($func->isProtected()) {
                    error_log('XML-RPC: ' . __METHOD__ . ': method to be wrapped is protected: ' . $plainFuncName);
                    return false;
                }
                if ($func->isConstructor()) {
                    error_log('XML-RPC: ' . __METHOD__ . ': method to be wrapped is the constructor: ' . $plainFuncName);
                    return false;
                }
                if ($func->isDestructor()) {
                    error_log('XML-RPC: ' . __METHOD__ . ': method to be wrapped is the destructor: ' . $plainFuncName);
                    return false;
                }
                if ($func->isAbstract()) {
                    error_log('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()) {
                // Note: from PHP 5.1.0 onward, we will possibly be able to use invokeargs
                // instead of getparameters to fully reflect internal php functions ?
                error_log('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
         *
         * @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)
         *
         * @param array $funcDesc as generated by self::introspectFunction()
         *
         * @return array
         */
        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
         * @todo validate params? In theory all validation is left to the dispatch map...
         * @todo add support for $catchWarnings
         *
         * @param $callable
         * @param array $extraOptions
         * @param string $plainFuncName
         * @param string $funcDesc
         * @return \Closure
         */
        protected function buildWrapFunctionClosure($callable, $extraOptions, $plainFuncName, $funcDesc)
        {
            $function = function($req) use($callable, $extraOptions, $funcDesc)
            {
                $nameSpace = '\\PhpXmlRpc\\';
                $encoderClass = $nameSpace.'Encoder';
                $responseClass = $nameSpace.'Response';
                $valueClass = $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)) {
                    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';
                        }

                        $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 xmlrpc 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
         *
         * @todo add a nice phpdoc block in the generated source
         */
        protected function buildWrapFunctionSource($callable, $newFuncName, $extraOptions, $plainFuncName, $funcDesc)
        {
            $namespace = '\\PhpXmlRpc\\';

            $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[] = "\$p[$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 {$namespace}Response(0, " . PhpXmlRpc::$xmlrpcerr['incorrect_params'] . ", '" . PhpXmlRpc::$xmlrpcstr['incorrect_params'] . "');\n";

            $innerCode .= "\$encoder = new {$namespace}Encoder();\n";
            if ($decodePhpObjects) {
                $innerCode .= "\$p = \$encoder->decode(\$req, array('decode_php_objs'));\n";
            } else {
                $innerCode .= "\$p = \$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 var...
            if (is_array($callable) && is_object($callable[0])) {
                self::$objHolder[$newFuncName] = $callable[0];
                $innerCode .= "\$obj = PhpXmlRpc\\Wrapper::\$objHolder['$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, '{$namespace}Response')) return \$retval; else\n";
            if ($funcDesc['returns'] == Value::$xmlrpcDateTime || $funcDesc['returns'] == Value::$xmlrpcBase64) {
                $innerCode .= "return new {$namespace}Response(new {$namespace}Value(\$retval, '{$funcDesc['returns']}'));";
            } else {
                if ($encodePhpObjects) {
                    $innerCode .= "return new {$namespace}Response(\$encoder->encode(\$retval, array('encode_php_objs')));\n";
                } else {
                    $innerCode .= "return new {$namespace}Response(\$encoder->encode(\$retval));\n";
                }
            }
            // shall we exclude functions returning by ref?
            // if($func->returnsReference())
            //     return false;

            $code = "function $newFuncName(\$req) {\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 xmlrpc methods from an xmlrpc 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 xmlrpc methods, or an object instance of that class
         * @param array $extraOptions see the docs for wrapPhpMethod 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 xmlrpc methods created
         *
         * @return array|false false on failure
         */
        public function wrapPhpClass($className, $extraOptions = array())
        {
            $methodFilter = isset($extraOptions['method_filter']) ? $extraOptions['method_filter'] : '';
            $methodType = isset($extraOptions['method_type']) ? $extraOptions['method_type'] : 'auto';
            $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : '';

            $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) {
                                if (is_object($className)) {
                                    $realClassName = get_class($className);
                                }else {
                                    $realClassName = $className;
                                }
                                $results[$prefix."$realClassName.$mName"] = $methodWrap;
                            }
                        }
                    }
                }
            }

            return $results;
        }

        /**
         * Given an xmlrpc client and a method name, register a php wrapper function
         * that will call it and return results using native php types for both
         * params and results. The generated php function will return a Response
         * object for failed xmlrpc calls.
         *
         * Known limitations:
         * - server must support system.methodsignature for the wanted xmlrpc 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)
         * - nested xmlrpc 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 datetime or 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 slow: a new xmlrpc client
         * is created on every invocation and an xmlrpc-connection opened+closed.
         * An extra 'debug' param is appended to param list of xmlrpc method, useful
         * for debugging purposes.
         *
         * @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 allow the created function to throw exceptions on method calls failures
         * @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...
         *
         * @param Client $client an xmlrpc client set up correctly to communicate with target server
         * @param string $methodName the xmlrpc 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' or 'https'
         *                            - 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 function itself (closure)
         *                            - bool    encode_php_objs     let php objects be sent to server using the 'improved' xmlrpc 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 xmlrpc 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    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|array|false false on failure, closure by default and array for return_source = true
         */
        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);
                /* was: $results = $this->build_remote_method_wrapper_code($client, $methodName,
                    $newFuncName, $mSig, $mDesc, $timeout, $protocol, $simpleClientCopy,
                    $prefix, $decodePhpObjects, $encodePhpObjects, $decodeFault,
                    $faultResponse, $namespace);*/

                $results['function'] = $newFuncName;

                return $results;
            }

        }

        /**
         * Retrieves an xmlrpc 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())
        {
            $namespace = '\\PhpXmlRpc\\';
            $reqClass = $namespace . 'Request';
            $valClass = $namespace . 'Value';
            $decoderClass = $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));
            $client->setDebug($debug);
            $response = $client->send($req, $timeout, $protocol);
            if ($response->faultCode()) {
                error_log('XML-RPC: ' . __METHOD__ . ': could not retrieve method signature from remote server for method ' . $methodName);
                return false;
            }

            $mSig = $response->value();
            if ($client->return_type != 'phpvals') {
                $decoder = new $decoderClass();
                $mSig = $decoder->decode($mSig);
            }

            if (!is_array($mSig) || count($mSig) <= $sigNum) {
                error_log('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())
        {
            $namespace = '\\PhpXmlRpc\\';
            $reqClass = $namespace . 'Request';
            $valClass = $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));
            $client->setDebug($debug);
            $response = $client->send($req, $timeout, $protocol);
            if (!$response->faultCode()) {
                $mDesc = $response->value();
                if ($client->return_type != 'phpvals') {
                    $mDesc = $mDesc->scalarval();
                }
            }

            return $mDesc;
        }

        /**
         * @param Client $client
         * @param string $methodName
         * @param array $extraOptions
         * @param string $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;
                if (isset($extraOptions['return_on_fault'])) {
                    $decodeFault = true;
                    $faultResponse = $extraOptions['return_on_fault'];
                } else {
                    $decodeFault = false;
                }

                $namespace = '\\PhpXmlRpc\\';
                $reqClass = $namespace . 'Request';
                $encoderClass = $namespace . 'Encoder';
                $valueClass = $namespace . 'Value';

                $encoder = new $encoderClass();
                $encodeOptions = array();
                if ($encodePhpObjects) {
                    $encodeOptions[] = 'encode_php_objs';
                }
                $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 == 'int' || $pType == 'boolean' || $pType == 'double' ||
                        $pType == 'string' || $pType == 'dateTime.iso8601' || $pType == 'base64' || $pType == 'null'
                    ) {
                        // by building directly xmlrpc values when type is known and scalar (instead of encode() calls),
                        // we make sure to honour the xmlrpc 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->return_type = 'xmlrpcvals';
                $resp = $clientClone->send($req, $timeout, $protocol);
                if ($resp->faultcode()) {
                    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;
        }

        /**
         * @param Client $client
         * @param string $methodName
         * @param array $extraOptions
         * @param string $newFuncName
         * @param array $mSig
         * @param string $mDesc
         * @return array
         */
        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;
            $clientCopyMode = isset($extraOptions['simple_client_copy']) ? (int)($extraOptions['simple_client_copy']) : 0;
            $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : 'xmlrpc';
            if (isset($extraOptions['return_on_fault'])) {
                $decodeFault = true;
                $faultResponse = $extraOptions['return_on_fault'];
            } else {
                $decodeFault = false;
                $faultResponse = '';
            }

            $namespace = '\\PhpXmlRpc\\';

            $code = "function $newFuncName (";
            if ($clientCopyMode < 2) {
                // client copy mode 0 or 1 == full / partial client copy in emitted code
                $verbatimClientCopy = !$clientCopyMode;
                $innerCode = $this->buildClientWrapperCode($client, $verbatimClientCopy, $prefix, $namespace);
                $innerCode .= "\$client->setDebug(\$debug);\n";
                $this_ = '';
            } else {
                // client copy mode 2 == no client copy in emitted code
                $innerCode = '';
                $this_ = 'this->';
            }
            $innerCode .= "\$req = new {$namespace}Request('$methodName');\n";

            if ($mDesc != '') {
                // take care that PHP comment is not terminated unwillingly by method description
                $mDesc = "/**\n* " . str_replace('*/', '* /', $mDesc) . "\n";
            } else {
                $mDesc = "/**\nFunction $newFuncName\n";
            }

            // param parsing
            $innerCode .= "\$encoder = new {$namespace}Encoder();\n";
            $plist = array();
            $pCount = count($mSig);
            for ($i = 1; $i < $pCount; $i++) {
                $plist[] = "\$p$i";
                $pType = $mSig[$i];
                if ($pType == 'i4' || $pType == 'int' || $pType == 'boolean' || $pType == 'double' ||
                    $pType == 'string' || $pType == 'dateTime.iso8601' || $pType == 'base64' || $pType == 'null'
                ) {
                    // only build directly xmlrpc values when type is known and scalar
                    $innerCode .= "\$p$i = new {$namespace}Value(\$p$i, '$pType');\n";
                } else {
                    if ($encodePhpObjects) {
                        $innerCode .= "\$p$i = \$encoder->encode(\$p$i, array('encode_php_objs'));\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]) . " (or an {$namespace}Response obj instance if call fails)\n*/\n";

            $innerCode .= "\$res = \${$this_}client->send(\$req, $timeout, '$protocol');\n";
            if ($decodeFault) {
                if (is_string($faultResponse) && ((strpos($faultResponse, '%faultCode%') !== false) || (strpos($faultResponse, '%faultString%') !== false))) {
                    $respCode = "str_replace(array('%faultCode%', '%faultString%'), array(\$res->faultCode(), \$res->faultString()), '" . str_replace("'", "''", $faultResponse) . "')";
                } else {
                    $respCode = var_export($faultResponse, true);
                }
            } else {
                $respCode = '$res';
            }
            if ($decodePhpObjects) {
                $innerCode .= "if (\$res->faultcode()) return $respCode; else return \$encoder->decode(\$res->value(), array('decode_php_objs'));";
            } else {
                $innerCode .= "if (\$res->faultcode()) return $respCode; else return \$encoder->decode(\$res->value());";
            }

            $code = $code . $plist . ") {\n" . $innerCode . "\n}\n";

            return array('source' => $code, 'docstring' => $mDesc);
        }

        /**
         * Similar to wrapXmlrpcMethod, but will generate a php class that wraps
         * all xmlrpc methods exposed by the remote server as own methods.
         * For more details see wrapXmlrpcMethod.
         *
         * For a slimmer alternative, see the code in demo/client/proxy.php
         *
         * Note that unlike wrapXmlrpcMethod, we always have to generate php code here. It seems that php 7 will have anon classes...
         *
         * @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 mixed false on error, the name of the created class if all ok or an array with code, class name and comments (if the appropriatevoption is set in extra_options)
         */
        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'] : '';
            $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;
            $buildIt = isset($extraOptions['return_source']) ? !($extraOptions['return_source']) : true;
            $prefix = isset($extraOptions['prefix']) ? $extraOptions['prefix'] : 'xmlrpc';
            $namespace = '\\PhpXmlRpc\\';

            $reqClass = $namespace . 'Request';
            $decoderClass = $namespace . 'Encoder';

            $req = new $reqClass('system.listMethods');
            $response = $client->send($req, $timeout, $protocol);
            if ($response->faultCode()) {
                error_log('XML-RPC: ' . __METHOD__ . ': could not retrieve method list from remote server');

                return false;
            } else {
                $mList = $response->value();
                if ($client->return_type != 'phpvals') {
                    $decoder = new $decoderClass();
                    $mList = $decoder->decode($mList);
                }
                if (!is_array($mList) || !count($mList)) {
                    error_log('XML-RPC: ' . __METHOD__ . ': could not retrieve meaningful method list from remote server');

                    return false;
                } else {
                    // pick a suitable name for the new function, avoiding collisions
                    if ($newClassName != '') {
                        $xmlrpcClassName = $newClassName;
                    } else {
                        $xmlrpcClassName = $prefix . '_' . preg_replace(array('/\./', '/[^a-zA-Z0-9_\x7f-\xff]/'),
                                array('_', ''), $client->server) . '_client';
                    }
                    while ($buildIt && class_exists($xmlrpcClassName)) {
                        $xmlrpcClassName .= 'x';
                    }

                    /// @todo add function setdebug() to new class, to enable/disable debugging
                    $source = "class $xmlrpcClassName\n{\npublic \$client;\n\n";
                    $source .= "function __construct()\n{\n";
                    $source .= $this->buildClientWrapperCode($client, $verbatimClientCopy, $prefix, $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_php_objs' => $encodePhpObjects,
                        'decode_php_objs' => $decodePhpObjects,
                        'prefix' => $prefix,
                    );
                    /// @todo build phpdoc for class definition, too
                    foreach ($mList as $mName) {
                        if ($methodFilter == '' || preg_match($methodFilter, $mName)) {
                            // note: 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['docstring'];
                                }
                                $source .= $methodWrap['source'] . "\n";
                            } else {
                                error_log('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 {
                            error_log('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 all of the state 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("'", "\'", $client->path) .
                "', '" . str_replace("'", "\'", $client->server) . "', $client->port);\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 as $fld => $val) {
                    if ($fld != 'debug' && $fld != 'return_type') {
                        $val = var_export($val, true);
                        $code .= "\$client->$fld = $val;\n";
                    }
                }
            }
            // only make sure that client always returns the correct data type
            $code .= "\$client->return_type = '{$prefix}vals';\n";
            //$code .= "\$client->setDebug(\$debug);\n";
            return $code;
        }
    }








}  // End of Namespace







namespace PhpXmlRpc\Helper
{
    //use PhpXmlRpc\PhpXmlRpc;

    use PhpXmlRpc\PhpXmlRpc;
    use PhpXmlRpc\Value;


    class Charset
    {
        // tables used for transcoding different charsets into us-ascii xml
        protected $xml_iso88591_Entities = array("in" => array(), "out" => array());

        /// @todo add to iso table the characters from cp_1252 range, i.e. 128 to 159?
        /// These 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...)
        /*
        protected $xml_cp1252_Entities = array('in' => array(), 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;'
        ));
        */

        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',),
        );

        protected static $instance = null;

        /**
         * This class is singleton for performance reasons.
         *
         * @return Charset
         */
        public static function instance()
        {
            if (self::$instance === null) {
                self::$instance = new self();
            }

            return self::$instance;
        }

        private function __construct()
        {
            for ($i = 0; $i < 32; $i++) {
                $this->xml_iso88591_Entities["in"][] = chr($i);
                $this->xml_iso88591_Entities["out"][] = "&#{$i};";
            }

            for ($i = 160; $i < 256; $i++) {
                $this->xml_iso88591_Entities["in"][] = chr($i);
                $this->xml_iso88591_Entities["out"][] = "&#{$i};";
            }

            /*for ($i = 128; $i < 160; $i++)
            {
                $this->xml_cp1252_Entities['in'][] = chr($i);
            }*/
        }

        /**
         * Convert a string to the correct XML representation in a 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 in the std case we are not sending a charset encoding mime type
         * along with http headers, so we are bound by RFC 3023 to emit strict us-ascii.
         *
         * @todo do a bit of basic benchmarking (strtr vs. str_replace)
         * @todo make usage of iconv() or recode_string() or mb_string() where available
         *
         * @param string $data
         * @param string $srcEncoding
         * @param string $destEncoding
         *
         * @return string
         */
        public function encodeEntities($data, $srcEncoding = '', $destEncoding = '')
        {
            if ($srcEncoding == '') {
                // lame, but we know no better...
                $srcEncoding = PhpXmlRpc::$xmlrpc_internalencoding;
            }

            switch (strtoupper($srcEncoding . '_' . $destEncoding)) {
                case 'ISO-8859-1_':
                case 'ISO-8859-1_US-ASCII':
                    $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 'ISO-8859-1_UTF-8':
                    $escapedData = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
                    $escapedData = utf8_encode($escapedData);
                    break;
                case 'ISO-8859-1_ISO-8859-1':
                case 'US-ASCII_US-ASCII':
                case 'US-ASCII_UTF-8':
                case 'US-ASCII_':
                case 'US-ASCII_ISO-8859-1':
                case 'UTF-8_UTF-8':
                //case 'CP1252_CP1252':
                    $escapedData = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
                    break;
                case 'UTF-8_':
                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 xmlrpc values out of different php types
                    $data = (string)$data;
                    $ns = strlen($data);
                    for ($nn = 0; $nn < $ns; $nn++) {
                        $ch = $data[$nn];
                        $ii = ord($ch);
                        //1 7 0bbbbbbb (127)
                        if ($ii < 128) {
                            /// @todo shall we replace this with a (supposedly) faster str_replace?
                            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
                        } //2 11 110bbbbb 10bbbbbb (2047)
                        elseif ($ii >> 5 == 6) {
                            $b1 = ($ii & 31);
                            $ii = ord($data[$nn + 1]);
                            $b2 = ($ii & 63);
                            $ii = ($b1 * 64) + $b2;
                            $ent = sprintf('&#%d;', $ii);
                            $escapedData .= $ent;
                            $nn += 1;
                        } //3 16 1110bbbb 10bbbbbb 10bbbbbb
                        elseif ($ii >> 4 == 14) {
                            $b1 = ($ii & 15);
                            $ii = ord($data[$nn + 1]);
                            $b2 = ($ii & 63);
                            $ii = ord($data[$nn + 2]);
                            $b3 = ($ii & 63);
                            $ii = ((($b1 * 64) + $b2) * 64) + $b3;
                            $ent = sprintf('&#%d;', $ii);
                            $escapedData .= $ent;
                            $nn += 2;
                        } //4 21 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb
                        elseif ($ii >> 3 == 30) {
                            $b1 = ($ii & 7);
                            $ii = ord($data[$nn + 1]);
                            $b2 = ($ii & 63);
                            $ii = ord($data[$nn + 2]);
                            $b3 = ($ii & 63);
                            $ii = ord($data[$nn + 3]);
                            $b4 = ($ii & 63);
                            $ii = ((((($b1 * 64) + $b2) * 64) + $b3) * 64) + $b4;
                            $ent = sprintf('&#%d;', $ii);
                            $escapedData .= $ent;
                            $nn += 3;
                        }
                    }
                    break;
                /*
                case 'CP1252_':
                case 'CP1252_US-ASCII':
                    $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':
                    $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 allone 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':
                    $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:
                    $escapedData = '';
                    error_log('XML-RPC: ' . __METHOD__ . ": Converting from $srcEncoding to $destEncoding: not supported...");
            }

            return $escapedData;
        }

        /**
         * 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.
         *
         * @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)
        {
            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
         * @deprecated
         *
         * @param string $charset
         *
         * @return array
         *
         * @throws \Exception for unknown/unsupported charsets
         */
        public function getEntities($charset)
        {
            switch ($charset)
            {
                case 'iso88591':
                    return $this->xml_iso88591_Entities;
                default:
                    throw new \Exception('Unsupported charset: ' . $charset);
            }
        }

    }







    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."
         *
         * These routines always assume localtime unless
         * $utc is set to 1, in which case UTC is assumed
         * and an adjustment for locale is made when encoding
         *
         * @param int $timet (timestamp)
         * @param int $utc (0 or 1)
         *
         * @return string
         */
        public static function iso8601Encode($timet, $utc = 0)
        {
            if (!$utc) {
                $t = strftime("%Y%m%dT%H:%M:%S", $timet);
            } else {
                if (function_exists('gmstrftime')) {
                    // gmstrftime doesn't exist in some versions
                    // of PHP
                    $t = gmstrftime("%Y%m%dT%H:%M:%S", $timet);
                } else {
                    $t = strftime("%Y%m%dT%H:%M:%S", $timet - date('Z'));
                }
            }

            return $t;
        }

        /**
         * Given an ISO8601 date string, return a timet in the localtime, or UTC.
         *
         * @param string $idate
         * @param int $utc either 0 or 1
         *
         * @return int (datetime)
         */
        public static function iso8601Decode($idate, $utc = 0)
        {
            $t = 0;
            if (preg_match('/([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})/', $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;
        }
    }






//use PhpXmlRpc\PhpXmlRpc;

    class Http
    {
        /**
         * Decode a string that is encoded with "chunked" transfer encoding as defined in rfc2068 par. 19.4.6
         * Code shamelessly stolen from nusoap library by Dietrich Ayala.
         *
         * @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 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
         *
         * @return array with keys 'headers' and 'cookies'
         * @throws \Exception
         */
        public function parseResponseHeaders(&$data, $headersProcessed = false, $debug=0)
        {
            $httpResponse = array('raw_data' => $data, 'headers'=> array(), 'cookies' => array());

            // Support "web-proxy-tunelling" 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 {
                    error_log('XML-RPC: ' . __METHOD__ . ': HTTPS via proxy error, tunnel connection possibly failed');
                    throw new \Exception(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)) {
                    // works fine in php 3, 4 and 5

                    break;
                }
                $data = substr($data, $pos);
            }
            if (!preg_match('/^HTTP\/[0-9.]+ 200 /', $data)) {
                $errstr = substr($data, 0, strpos($data, "\n") - 1);
                error_log('XML-RPC: ' . __METHOD__ . ': HTTP error, got response: ' . $errstr);
                throw new \Exception(PhpXmlRpc::$xmlrpcstr['http_error'] . ' (' . $errstr . ')', PhpXmlRpc::$xmlrpcerr['http_error']);
            }

            // 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)));
            // while (list(, $line) = @each($ar)) {
            foreach ($ar as $tossKey => $line) {
                // take care of multi-line headers and cookies
                $arr = explode(':', $line, 2);
                if (count($arr) > 1) {
                    $headerName = strtolower(trim($arr[0]));
                    /// @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...
                    if ($headerName == 'set-cookie' || $headerName == 'set-cookie2') {
                        if ($headerName == 'set-cookie2') {
                            // version 2 cookies:
                            // there could be many cookies on one line, comma separated
                            $cookies = explode(',', $arr[1]);
                        } else {
                            $cookies = array($arr[1]);
                        }
                        foreach ($cookies as $cookie) {
                            // 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
                            // feature creep: only allow rfc-compliant cookie attributes?
                            // @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 = trim(@$val[1]);
                                /// @todo with version 1 cookies, we should strip leading and trailing " chars
                                if ($pos == 0) {
                                    $cookiename = $tag;
                                    $httpResponse['cookies'][$tag] = array();
                                    $httpResponse['cookies'][$cookiename]['value'] = urldecode($val);
                                } else {
                                    if ($tag != 'value') {
                                        $httpResponse['cookies'][$cookiename][$tag] = $val;
                                    }
                                }
                            }
                        }
                    } else {
                        $httpResponse['headers'][$headerName] = trim($arr[1]);
                    }
                } elseif (isset($headerName)) {
                    /// @todo version1 cookies might span multiple lines, thus breaking the parsing above
                    $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";
                }
                Logger::instance()->debugMessage($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 = Http::decodeChunked($data)) {
                        error_log('XML-RPC: ' . __METHOD__ . ': errors occurred when trying to rebuild the chunked data received from server');
                        throw new \Exception(PhpXmlRpc::$xmlrpcstr['dechunk_fail'], PhpXmlRpc::$xmlrpcerr['dechunk_fail']);
                    }
                }

                // 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) {
                                    Logger::instance()->debugMessage("---INFLATED RESPONSE---[" . strlen($data) . " chars]---\n$data\n---END---");
                                }
                            } elseif ($httpResponse['headers']['content-encoding'] == 'gzip' && $degzdata = @gzinflate(substr($data, 10))) {
                                $data = $degzdata;
                                if ($debug) {
                                    Logger::instance()->debugMessage("---INFLATED RESPONSE---[" . strlen($data) . " chars]---\n$data\n---END---");
                                }
                            } else {
                                error_log('XML-RPC: ' . __METHOD__ . ': errors occurred when trying to decode the deflated data received from server');
                                throw new \Exception(PhpXmlRpc::$xmlrpcstr['decompress_fail'], PhpXmlRpc::$xmlrpcerr['decompress_fail']);
                            }
                        } else {
                            error_log('XML-RPC: ' . __METHOD__ . ': the server sent deflated data. Your php install must have the Zlib extension compiled in to support this.');
                            throw new \Exception(PhpXmlRpc::$xmlrpcstr['cannot_decompress'], PhpXmlRpc::$xmlrpcerr['cannot_decompress']);
                        }
                    }
                }
            } // end of 'if needed, de-chunk, re-inflate response'

            return $httpResponse;
        }
    }





    class Logger
    {
        protected static $instance = null;

        /**
         * This class is singleton, so that later we can move to DI patterns.
         *
         * @return Logger
         */
        public static function instance()
        {
            if (self::$instance === null) {
                self::$instance = new self();
            }

            return self::$instance;
        }

        /**
         * 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
         */
        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 | ENT_HTML401 | 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();
        }
    }





    //use PhpXmlRpc\PhpXmlRpc;
    //use PhpXmlRpc\Value;

    /**
     * Deals with parsing the XML.
     */
    class XMLParser
    {
        // used to store state during parsing
        // quick explanation of components:
        //   ac - used to accumulate values
        //   stack - array with genealogy of xml elements names:
        //           used to validate nesting of xmlrpc 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
        //   isf - used to indicate a parsing fault (2) or xmlrpc response fault (1)
        //   isf_reason - used for storing xmlrpc response fault string
        //   method - used to store method name
        //   params - used to store parameters in method calls
        //   pt - used to store the type of each received parameter. Useful if parameters are automatically decoded to php values
        //   rt  - 'methodcall or 'methodresponse'
        public $_xh = array(
            'ac' => '',
            'stack' => array(),
            'valuestack' => array(),
            'isf' => 0,
            'isf_reason' => '',
            'method' => false, // so we can check later if we got a methodname or not
            'params' => array(),
            'pt' => array(),
            'rt' => '',
        );

        public $xmlrpc_valid_parents = array(
            'VALUE' => array('MEMBER', 'DATA', 'PARAM', 'FAULT'),
            'BOOLEAN' => array('VALUE'),
            'I4' => 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
        );

        /**
         * xml parser handler function for opening element tags.
         */
        public function xmlrpc_se($parser, $name, $attrs, $acceptSingleVals = false)
        {
            // if invalid xmlrpc already detected, skip all processing
            if ($this->_xh['isf'] < 2) {
                // check for correct element nesting
                // 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
                if (count($this->_xh['stack']) == 0) {
                    if ($name != 'METHODRESPONSE' && $name != 'METHODCALL' && (
                            $name != 'VALUE' && !$acceptSingleVals)
                    ) {
                        $this->_xh['isf'] = 2;
                        $this->_xh['isf_reason'] = 'missing top level xmlrpc element';

                        return;
                    } else {
                        $this->_xh['rt'] = strtolower($name);
                    }
                } 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 '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();
                        $curVal['values'] = array();
                        $curVal['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':
                        $this->_xh['valuestack'][count($this->_xh['valuestack']) - 1]['name'] = ''; // set member name to null, in case we do not find in the xml later on
                        //$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 (PhpXmlRpc::$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;
                            }
                            $this->_xh['ac'] = ''; // reset the accumulator
                            break;
                        }
                    // we do not support the <NIL/> extension, so
                    // drop through intentionally
                    default:
                        /// INVALID ELEMENT: RAISE ISF so that it is later recognized!!!
                        $this->_xh['isf'] = 2;
                        $this->_xh['isf_reason'] = "found not-xmlrpc xml element $name";
                        break;
                }

                // 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;
                }
            }
        }

        /**
         * Used in decoding xml chunks that might represent single xmlrpc values.
         */
        public function xmlrpc_se_any($parser, $name, $attrs)
        {
            $this->xmlrpc_se($parser, $name, $attrs, true);
        }

        /**
         * xml parser handler function for close element tags.
         */
        public function xmlrpc_ee($parser, $name, $rebuildXmlrpcvals = true)
        {
            if ($this->_xh['isf'] < 2) {
                // 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':
                        // This if() detects if no scalar was inside <VALUE></VALUE>
                        if ($this->_xh['vt'] == 'value') {
                            $this->_xh['value'] = $this->_xh['ac'];
                            $this->_xh['vt'] = Value::$xmlrpcString;
                        }

                        if ($rebuildXmlrpcvals) {
                            // build the xmlrpc 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'];
                            }
                            // 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'][] = $temp;
                            } else {
                                $this->_xh['value'] = $temp;
                            }
                        } else {
                            /// @todo this needs to treat correctly 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 'BOOLEAN':
                    case 'I4':
                    case 'INT':
                    case 'STRING':
                    case 'DOUBLE':
                    case 'DATETIME.ISO8601':
                    case 'BASE64':
                        $this->_xh['vt'] = strtolower($name);
                        /// @todo: optimization creep - remove the if/elseif cycle below
                        /// since the case() in which we are already did that
                        if ($name == 'STRING') {
                            $this->_xh['value'] = $this->_xh['ac'];
                        } elseif ($name == 'DATETIME.ISO8601') {
                            if (!preg_match('/^[0-9]{8}T[0-9]{2}:[0-9]{2}:[0-9]{2}$/', $this->_xh['ac'])) {
                                error_log('XML-RPC: ' . __METHOD__ . ': invalid value received in DATETIME: ' . $this->_xh['ac']);
                            }
                            $this->_xh['vt'] = Value::$xmlrpcDateTime;
                            $this->_xh['value'] = $this->_xh['ac'];
                        } elseif ($name == 'BASE64') {
                            /// @todo check for failure of base64 decoding / catch warnings
                            $this->_xh['value'] = base64_decode($this->_xh['ac']);
                        } elseif ($name == 'BOOLEAN') {
                            // special case here: 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 eg. Blogger api docs)
                            // NB: this simple checks helps a lot sanitizing input, ie no
                            // security problems around here
                            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
                                if ($this->_xh['ac'] != '0' && strcasecmp($this->_xh['ac'], 'false') != 0) {
                                    error_log('XML-RPC: ' . __METHOD__ . ': invalid value received in BOOLEAN: ' . $this->_xh['ac']);
                                }
                                $this->_xh['value'] = false;
                            }
                        } elseif ($name == 'DOUBLE') {
                            // we have a DOUBLE
                            // we must check that only 0123456789-.<space> are characters here
                            // NOTE: regexp could be much stricter than this...
                            if (!preg_match('/^[+-eE0123456789 \t.]+$/', $this->_xh['ac'])) {
                                /// @todo: find a better way of throwing an error than this!
                                error_log('XML-RPC: ' . __METHOD__ . ': non numeric value received in DOUBLE: ' . $this->_xh['ac']);
                                $this->_xh['value'] = 'ERROR_NON_NUMERIC_FOUND';
                            } else {
                                // it's ok, add it on
                                $this->_xh['value'] = (double)$this->_xh['ac'];
                            }
                        } else {
                            // we have an I4/INT
                            // we must check that only 0123456789-<space> are characters here
                            if (!preg_match('/^[+-]?[0123456789 \t]+$/', $this->_xh['ac'])) {
                                /// @todo find a better way of throwing an error than this!
                                error_log('XML-RPC: ' . __METHOD__ . ': non numeric value received in INT: ' . $this->_xh['ac']);
                                $this->_xh['value'] = 'ERROR_NON_NUMERIC_FOUND';
                            } else {
                                // it's ok, add it on
                                $this->_xh['value'] = (int)$this->_xh['ac'];
                            }
                        }
                        $this->_xh['lv'] = 3; // indicate we've found a value
                        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 was found
                        if ($this->_xh['vt']) {
                            $vscount = count($this->_xh['valuestack']);
                            $this->_xh['valuestack'][$vscount - 1]['values'][$this->_xh['valuestack'][$vscount - 1]['name']] = $this->_xh['value'];
                        } else {
                            error_log('XML-RPC: ' . __METHOD__ . ': missing VALUE inside STRUCT in received xml');
                        }
                        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
                        if ($this->_xh['vt']) {
                            $this->_xh['params'][] = $this->_xh['value'];
                            $this->_xh['pt'][] = $this->_xh['vt'];
                        } else {
                            error_log('XML-RPC: ' . __METHOD__ . ': missing VALUE inside PARAM in received xml');
                        }
                        break;
                    case 'METHODNAME':
                        $this->_xh['method'] = preg_replace('/^[\n\r\t ]+/', '', $this->_xh['ac']);
                        break;
                    case 'NIL':
                    case 'EX:NIL':
                        if (PhpXmlRpc::$xmlrpc_null_extension) {
                            $this->_xh['vt'] = 'null';
                            $this->_xh['value'] = null;
                            $this->_xh['lv'] = 3;
                            break;
                        }
                    // drop through intentionally if nil extension not enabled
                    case 'PARAMS':
                    case 'FAULT':
                    case 'METHODCALL':
                    case 'METHORESPONSE':
                        break;
                    default:
                        // End of INVALID ELEMENT!
                        // shall we add an assert here for unreachable code???
                        break;
                }
            }
        }

        /**
         * Used in decoding xmlrpc requests/responses without rebuilding xmlrpc Values.
         */
        public function xmlrpc_ee_fast($parser, $name)
        {
            $this->xmlrpc_ee($parser, $name, false);
        }

        /**
         * xml parser handler function for character data.
         */
        public function xmlrpc_cd($parser, $data)
        {
            // skip processing if xml fault already detected
            if ($this->_xh['isf'] < 2) {
                // "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', ie. not char data or
         * element start/end tag. In fact it only gets called on unknown entities...
         */
        public function xmlrpc_dh($parser, $data)
        {
            // skip processing if xml fault already detected
            if ($this->_xh['isf'] < 2) {
                if (substr($data, 0, 1) == '&' && substr($data, -1, 1) == ';') {
                    $this->_xh['ac'] .= $data;
                }
            }

            return true;
        }

        /**
         * 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 explore usage of mb_http_input(): does it detect http headers + post data? if so, use it instead of hand-detection!!!
         */
        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
            // 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 (extension_loaded('mbstring')) {
                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: as per HTTP1.1 assume it is iso-8859-1?
                // Both RFC 2616 (HTTP 1.1) and 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...
                return PhpXmlRpc::$xmlrpc_defencoding;
            }
        }

        /**
         * Helper function: checks if an xml chunk as a charset declaration (BOM or in the xml declaration)
         *
         * @param string $xmlChunk
         * @return bool
         */
        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, $matches)) {
                return true;
            }

            return false;
        }
    }




}