fix mixed-calling-convention servers; prefer return-early; reformat comments
authorgggeek <giunta.gaetano@gmail.com>
Sun, 15 Jan 2023 23:20:39 +0000 (23:20 +0000)
committergggeek <giunta.gaetano@gmail.com>
Sun, 15 Jan 2023 23:20:39 +0000 (23:20 +0000)
demo/server/methodProviders/testsuite.php
src/Helper/XMLParser.php
src/Server.php

index 45c26db..2c6f2f1 100644 (file)
@@ -35,18 +35,20 @@ function getAllHeaders_xmlrpc($req)
     }
 }
 
+// used to test mixed-convention calling
 $setcookies_sig = array(array(Value::$xmlrpcInt, Value::$xmlrpcStruct));
 $setcookies_doc = 'Sends to client a response containing a single \'1\' digit, and sets to it http cookies as received in the request (array of structs describing a cookie)';
-function setCookies($req)
+function setCookies($cookies)
 {
-    $encoder = new Encoder();
-    $cookies = $req->getParam(0);
-    foreach ($cookies as $name => $value) {
-        $cookieDesc = $encoder->decode($value);
-        setcookie($name, @$cookieDesc['value'], @$cookieDesc['expires'], @$cookieDesc['path'], @$cookieDesc['domain'], @$cookieDesc['secure']);
+    foreach ($cookies as $name => $cookieDesc) {
+        if (is_array($cookieDesc)) {
+            setcookie($name, @$cookieDesc['value'], @$cookieDesc['expires'], @$cookieDesc['path'], @$cookieDesc['domain'], @$cookieDesc['secure']);
+        } else {
+            /// @todo
+        }
     }
 
-    return new Response(new Value(1, Value::$xmlrpcInt));
+    return 1;
 }
 
 $getcookies_sig = array(array(Value::$xmlrpcStruct));
@@ -83,6 +85,7 @@ return array(
         "function" => 'setCookies',
         "signature" => $setcookies_sig,
         "docstring" => $setcookies_doc,
+        "parameters_type" => 'phpvals',
     ),
     "tests.getcookies" => array(
         "function" => 'getCookies',
index 6da8ef9..b6f3c04 100644 (file)
@@ -11,10 +11,12 @@ use PhpXmlRpc\Value;
  *
  * @todo implement an interface to allow for alternative implementations
  *       - make access to $_xh protected, return more high-level data structures
+ *       - move $this->accept, $this->callbacks to an internal-use parsing-options config, along with the private
+ *         parts of $_xh
  *       - add parseRequest, parseResponse, parseValue methods
  * @todo if iconv() or mb_string() are available, we could allow to convert the received xml to a custom charset encoding
  *       while parsing, which is faster than doing it later by going over the rebuilt data structure
- * @todo allow usage of a custom Logger via the DIC(ish) pattern we use in other classes
+ * @todo allow to parse data from a stream, to avoid having to copy first the whole xml to memory
  */
 class XMLParser
 {
@@ -29,21 +31,26 @@ class XMLParser
 
     protected static $logger;
 
-    // Used to store state during parsing and to pass parsing results to callers.
-    // Quick explanation of components:
-    //  private:
-    //    ac - used to accumulate values
-    //    stack - array with genealogy of xml elements names used to validate nesting of 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
-    //  public:
-    //    isf - used to indicate an xml parsing fault (3), invalid xmlrpc fault (2) or xmlrpc response fault (1)
-    //    isf_reason - used for storing xmlrpc response fault string
-    //    value - used to store the value in responses
-    //    method - used to store method name in requests
-    //    params - used to store parameters in requests
-    //    pt - used to store the type of each received parameter. Useful if parameters are automatically decoded to php values
-    //    rt  - 'methodcall', 'methodresponse', 'value' or 'fault' (the last one used only in EPI emulation mode)
+    /**
+     * @var array
+     * Used to store state during parsing and to pass parsing results to callers.
+     * Quick explanation of components:
+     *  private:
+     *    ac - used to accumulate values
+     *    stack - array with genealogy of xml elements names, used to validate nesting of 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
+     *         (values: 0=not looking, 1=looking, 3=found)
+     *  public:
+     *    isf - used to indicate an xml-rpc response fault (1), invalid xml-rpc fault (2), xml parsing fault (3) or
+     *          bad parameters passed to the parsing call (4)
+     *    isf_reason - used for storing xml-rpc response fault string
+     *    value - used to store the value in responses
+     *    method - used to store method name in requests
+     *    params - used to store parameters in requests
+     *    pt - used to store the type of each received parameter. Useful if parameters are automatically decoded to php values
+     *    rt - 'methodcall', 'methodresponse', 'value' or 'fault' (the last one used only in EPI emulation mode)
+     */
     public $_xh = array(
         'ac' => '',
         'stack' => array(),
@@ -57,6 +64,10 @@ class XMLParser
         'rt' => '',
     );
 
+    /**
+     * @var array[]
+     * @internal
+     */
     public $xmlrpc_valid_parents = array(
         'VALUE' => array('MEMBER', 'DATA', 'PARAM', 'FAULT'),
         'BOOLEAN' => array('VALUE'),
@@ -81,12 +92,14 @@ class XMLParser
         'EX:NIL' => array('VALUE'), // only used when extension activated
     );
 
-    /** @var array $parsing_options */
+    /** @var int[] $parsing_options */
     protected $parsing_options = array();
     /** @var int $accept self::ACCEPT_REQUEST | self::ACCEPT_RESPONSE by default */
     protected $accept = 3;
     /** @var int $maxChunkLength 4 MB by default. Any value below 10MB should be good */
     protected $maxChunkLength = 4194304;
+    /** @var \Callable[] */
+    protected $callbacks = array();
 
     public function getLogger()
     {
@@ -106,7 +119,7 @@ class XMLParser
     }
 
     /**
-     * @param array $options passed to the xml parser
+     * @param int[] $options passed to the xml parser
      */
     public function __construct(array $options = array())
     {
@@ -115,9 +128,10 @@ class XMLParser
 
     /**
      * @param string $data
-     * @param string $returnType
+     * @param string $returnType self::RETURN_XMLRPCVALS, self::RETURN_PHP, self::RETURN_EPIVALS
      * @param int $accept a bit-combination of self::ACCEPT_REQUEST, self::ACCEPT_RESPONSE, self::ACCEPT_VALUE
-     * @param array $options passed to the xml parser, in addition to the options received in the constructor
+     * @param array $options integer-key options are passed to the xml parser, in addition to the options received in
+     *                       the constructor. String-key options are used independently
      * @return void
      */
     public function parse($data, $returnType = self::RETURN_XMLRPCVALS, $accept = 3, $options = array())
@@ -144,6 +158,29 @@ class XMLParser
             return;
         }
 
+        $prevAccept = $this->accept;
+        $this->accept = $accept;
+
+        $this->callbacks = array();
+        foreach ($options as $key => $val) {
+            if (is_string($key)) {
+                switch($key) {
+                    case 'methodname_callback':
+                        if (!is_callable($val)) {
+                            $this->_xh['isf'] = 4;
+                            $this->_xh['isf_reason'] = "Callback passed as 'methodname_callback' is not callable";
+                            return;
+                        } else {
+                            $this->callbacks['methodname'] = $val;
+                        }
+                        break;
+                    default:
+                        $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ": unsupported option: $key");
+                }
+                unset($options[$key]);
+            }
+        }
+
         // NB: we use '' instead of null to force charset detection from the xml declaration
         $parser = xml_parser_create('');
 
@@ -165,6 +202,8 @@ class XMLParser
             case self::RETURN_EPIVALS:
                 xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_epi');
                 break;
+            /// @todo log a warning on unsupported return type
+            case XMLParser::RETURN_XMLRPCVALS:
             default:
                 xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee');
         }
@@ -172,24 +211,36 @@ class XMLParser
         xml_set_character_data_handler($parser, 'xmlrpc_cd');
         xml_set_default_handler($parser, 'xmlrpc_dh');
 
-        $this->accept = $accept;
-
-        // @see ticket #70 - we have to parse big xml docks in chunks to avoid errors
-        for ($offset = 0; $offset < $len; $offset += $this->maxChunkLength) {
-            $chunk = substr($data, $offset, $this->maxChunkLength);
-            // error handling: xml not well formed
-            if (!xml_parse($parser, $chunk, $offset + $this->maxChunkLength >= $len)) {
-                $errCode = xml_get_error_code($parser);
-                $errStr = sprintf('XML error %s: %s at line %d, column %d', $errCode, xml_error_string($errCode),
-                    xml_get_current_line_number($parser), xml_get_current_column_number($parser));
-
-                $this->_xh['isf'] = 3;
-                $this->_xh['isf_reason'] = $errStr;
-                break;
+        try {
+            // @see ticket #70 - we have to parse big xml docs in chunks to avoid errors
+            for ($offset = 0; $offset < $len; $offset += $this->maxChunkLength) {
+                $chunk = substr($data, $offset, $this->maxChunkLength);
+                // error handling: xml not well formed
+                if (!xml_parse($parser, $chunk, $offset + $this->maxChunkLength >= $len)) {
+                    $errCode = xml_get_error_code($parser);
+                    $errStr = sprintf('XML error %s: %s at line %d, column %d', $errCode, xml_error_string($errCode),
+                        xml_get_current_line_number($parser), xml_get_current_column_number($parser));
+
+                    $this->_xh['isf'] = 3;
+                    $this->_xh['isf_reason'] = $errStr;
+                    break;
+                }
+                // no need to parse further if we already have a fatal error
+                if ($this->_xh['isf'] >= 2) {
+                    break;
+                }
             }
+        } catch (\Exception $e) {
+            xml_parser_free($parser);
+            $this->callbacks = array();
+            $this->accept = $prevAccept;
+            /// @todo should we set $this->_xh['isf'] and $this->_xh['isf_reason'] ?
+            throw $e;
         }
 
         xml_parser_free($parser);
+        $this->callbacks = array();
+        $this->accept = $prevAccept;
     }
 
     /**
@@ -201,82 +252,148 @@ class XMLParser
      * @param $attrs
      * @param bool $acceptSingleVals DEPRECATED use the $accept parameter instead
      * @return void
+     *
+     * @todo optimization: throw when setting $this->_xh['isf'] > 1, to completely avoid further xml parsing
      */
     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
-            if (count($this->_xh['stack']) == 0) {
-                // top level element can only be of 2 types
-                /// @todo optimization creep: save this check into a bool variable, instead of using count() every time:
-                ///       there is only a single top level element in xml anyway
-                // BC
-                if ($acceptSingleVals === false) {
-                    $accept = $this->accept;
-                } else {
-                    $accept = self::ACCEPT_REQUEST | self::ACCEPT_RESPONSE | self::ACCEPT_VALUE;
+        if ($this->_xh['isf'] >= 2) {
+            return;
+        }
+
+        // check for correct element nesting
+        if (count($this->_xh['stack']) == 0) {
+            // top level element can only be of 2 types
+            /// @todo optimization creep: save this check into a bool variable, instead of using count() every time:
+            ///       there is only a single top level element in xml anyway
+            // BC
+            if ($acceptSingleVals === false) {
+                $accept = $this->accept;
+            } else {
+                $accept = self::ACCEPT_REQUEST | self::ACCEPT_RESPONSE | self::ACCEPT_VALUE;
+            }
+            if (($name == 'METHODCALL' && ($accept & self::ACCEPT_REQUEST)) ||
+                ($name == 'METHODRESPONSE' && ($accept & self::ACCEPT_RESPONSE)) ||
+                ($name == 'VALUE' && ($accept & self::ACCEPT_VALUE)) ||
+                ($name == 'FAULT' && ($accept & self::ACCEPT_FAULT))) {
+                $this->_xh['rt'] = strtolower($name);
+            } else {
+                $this->_xh['isf'] = 2;
+                $this->_xh['isf_reason'] = 'missing top level xmlrpc element. Found: ' . $name;
+
+                return;
+            }
+        } else {
+            // not top level element: see if parent is OK
+            $parent = end($this->_xh['stack']);
+            if (!array_key_exists($name, $this->xmlrpc_valid_parents) || !in_array($parent, $this->xmlrpc_valid_parents[$name])) {
+                $this->_xh['isf'] = 2;
+                $this->_xh['isf_reason'] = "xmlrpc element $name cannot be child of $parent";
+
+                return;
+            }
+        }
+
+        switch ($name) {
+            // optimize for speed switch cases: most common cases first
+            case 'VALUE':
+                /// @todo we could check for 2 VALUE elements inside a MEMBER or PARAM element
+                $this->_xh['vt'] = 'value'; // indicator: no value found yet
+                $this->_xh['ac'] = '';
+                $this->_xh['lv'] = 1;
+                $this->_xh['php_class'] = null;
+                break;
+
+            case 'I8':
+            case 'EX:I8':
+                if (PHP_INT_SIZE === 4) {
+                    // INVALID ELEMENT: RAISE ISF so that it is later recognized!!!
+                    $this->_xh['isf'] = 2;
+                    $this->_xh['isf_reason'] = "Received i8 element but php is compiled in 32 bit mode";
+
+                    return;
                 }
-                if (($name == 'METHODCALL' && ($accept & self::ACCEPT_REQUEST)) ||
-                    ($name == 'METHODRESPONSE' && ($accept & self::ACCEPT_RESPONSE)) ||
-                    ($name == 'VALUE' && ($accept & self::ACCEPT_VALUE)) ||
-                    ($name == 'FAULT' && ($accept & self::ACCEPT_FAULT))) {
-                    $this->_xh['rt'] = strtolower($name);
-                } else {
+                // fall through voluntarily
+
+            case 'I4':
+            case 'INT':
+            case 'STRING':
+            case 'BOOLEAN':
+            case 'DOUBLE':
+            case 'DATETIME.ISO8601':
+            case 'BASE64':
+                if ($this->_xh['vt'] != 'value') {
+                    // two data elements inside a value: an error occurred!
                     $this->_xh['isf'] = 2;
-                    $this->_xh['isf_reason'] = 'missing top level xmlrpc element. Found: ' . $name;
+                    $this->_xh['isf_reason'] = "$name element following a {$this->_xh['vt']} element inside a single value";
 
                     return;
                 }
-            } else {
-                // not top level element: see if parent is OK
-                $parent = end($this->_xh['stack']);
-                if (!array_key_exists($name, $this->xmlrpc_valid_parents) || !in_array($parent, $this->xmlrpc_valid_parents[$name])) {
+                $this->_xh['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'] = "xmlrpc element $name cannot be child of $parent";
+                    $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;
 
-            switch ($name) {
-                // optimize for speed switch cases: most common cases first
-                case 'VALUE':
-                    /// @todo we could check for 2 VALUE elements inside a MEMBER or PARAM element
-                    $this->_xh['vt'] = 'value'; // indicator: no value found yet
-                    $this->_xh['ac'] = '';
-                    $this->_xh['lv'] = 1;
-                    $this->_xh['php_class'] = null;
-                    break;
-                case 'I8':
-                case 'EX:I8':
-                    if (PHP_INT_SIZE === 4) {
-                        // INVALID ELEMENT: RAISE ISF so that it is later recognized!!!
-                        $this->_xh['isf'] = 2;
-                        $this->_xh['isf_reason'] = "Received i8 element but php is compiled in 32 bit mode";
+            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;
-                    }
-                    // fall through voluntarily
-                case 'I4':
-                case 'INT':
-                case 'STRING':
-                case 'BOOLEAN':
-                case 'DOUBLE':
-                case 'DATETIME.ISO8601':
-                case 'BASE64':
-                    if ($this->_xh['vt'] != 'value') {
-                        // two data elements inside a value: an error occurred!
-                        $this->_xh['isf'] = 2;
-                        $this->_xh['isf_reason'] = "$name element following a {$this->_xh['vt']} element inside a single value";
+                    return;
+                }
 
-                        return;
-                    }
-                    $this->_xh['ac'] = ''; // reset the accumulator
-                    break;
-                case 'STRUCT':
-                case 'ARRAY':
+            case 'METHODCALL':
+            case 'METHODRESPONSE':
+            case 'PARAMS':
+                // valid elements that add little to processing
+                break;
+
+            case 'METHODNAME':
+            case 'NAME':
+                /// @todo we could check for 2 NAME elements inside a MEMBER element
+                $this->_xh['ac'] = '';
+                break;
+
+            case 'FAULT':
+                $this->_xh['isf'] = 1;
+                break;
+
+            case 'MEMBER':
+                // set member name to null, in case we do not find in the xml later on
+                $this->_xh['valuestack'][count($this->_xh['valuestack']) - 1]['name'] = '';
+                //$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;
@@ -284,77 +401,27 @@ class XMLParser
 
                         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
+                    // reset the accumulator - q: is this necessary at all here?
                     $this->_xh['ac'] = '';
                     break;
-                case 'FAULT':
-                    $this->_xh['isf'] = 1;
-                    break;
-                case 'MEMBER':
-                    // set member name to null, in case we do not find in the xml later on
-                    $this->_xh['valuestack'][count($this->_xh['valuestack']) - 1]['name'] = '';
-                    //$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;
-                    }
+                }
                 // if here, 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;
+            default:
+                // INVALID ELEMENT: RAISE ISF so that it is later recognized
+                /// @todo feature creep = allow a callback instead
+                $this->_xh['isf'] = 2;
+                $this->_xh['isf_reason'] = "found not-xmlrpc xml element $name";
+                break;
+        }
 
-            /// @todo optimization creep: move this inside the big switch() above
-            if ($name != 'VALUE') {
-                $this->_xh['lv'] = 0;
-            }
+        // 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;
         }
     }
 
@@ -383,181 +450,193 @@ class XMLParser
      */
     public function xmlrpc_ee($parser, $name, $rebuildXmlrpcvals = 1)
     {
-        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 ($this->_xh['isf'] >= 2) {
+            return;
 
-                    if ($rebuildXmlrpcvals > 0) {
-                        // 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'];
-                        }
-                        $this->_xh['value'] = $temp;
-                    } elseif ($rebuildXmlrpcvals < 0) {
-                        if ($this->_xh['vt'] == Value::$xmlrpcDateTime) {
-                            $this->_xh['value'] = (object)array(
-                                'xmlrpc_type' => 'datetime',
-                                'scalar' => $this->_xh['value'],
-                                'timestamp' => \PhpXmlRpc\Helper\Date::iso8601Decode($this->_xh['value'])
-                            );
-                        } elseif ($this->_xh['vt'] == Value::$xmlrpcBase64) {
-                            $this->_xh['value'] = (object)array(
-                                'xmlrpc_type' => 'base64',
-                                'scalar' => $this->_xh['value']
-                            );
-                        }
-                    } else {
-                        /// @todo this should handle php-serialized objects,
-                        /// since std deserializing is done by php_xmlrpc_decode,
-                        /// which we will not be calling...
-                        //if (isset($this->_xh['php_class'])) {
-                        //}
+        }
+        // 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 > 0) {
+                    // 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'];
+                    }
+                    $this->_xh['value'] = $temp;
+                } elseif ($rebuildXmlrpcvals < 0) {
+                    if ($this->_xh['vt'] == Value::$xmlrpcDateTime) {
+                        $this->_xh['value'] = (object)array(
+                            'xmlrpc_type' => 'datetime',
+                            'scalar' => $this->_xh['value'],
+                            'timestamp' => \PhpXmlRpc\Helper\Date::iso8601Decode($this->_xh['value'])
+                        );
+                    } elseif ($this->_xh['vt'] == Value::$xmlrpcBase64) {
+                        $this->_xh['value'] = (object)array(
+                            'xmlrpc_type' => 'base64',
+                            'scalar' => $this->_xh['value']
+                        );
                     }
+                } else {
+                    /// @todo this should handle php-serialized objects, since std deserializing is done
+                    ///  by php_xmlrpc_decode, which we will not be calling...
+                    //if (isset($this->_xh['php_class'])) {
+                    //}
+                }
 
-                    // check if we are inside an array or struct:
-                    // if value just built is inside an array, let's move it into array on the stack
-                    $vscount = count($this->_xh['valuestack']);
-                    if ($vscount && $this->_xh['valuestack'][$vscount - 1]['type'] == 'ARRAY') {
-                        $this->_xh['valuestack'][$vscount - 1]['values'][] = $this->_xh['value'];
+                // 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 'I8':
+            case 'EX:I8':
+            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'])) {
+                        $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': invalid value received in DATETIME: ' . $this->_xh['ac']);
                     }
-                    break;
-                case 'BOOLEAN':
-                case 'I4':
-                case 'I8':
-                case 'EX:I8':
-                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'])) {
-                            $this->getLogger()->errorLog('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) {
-                                $this->getLogger()->errorLog('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!
-                            $this->getLogger()->errorLog('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'];
-                        }
+                    $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 {
-                        // we have an I4/I8/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!
-                            $this->getLogger()->errorLog('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'];
+                        // 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) {
+                            $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': invalid value received in BOOLEAN: ' . $this->_xh['ac']);
                         }
+                        $this->_xh['value'] = false;
                     }
-                    $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'];
+                } 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!
+                        $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': non numeric value received in DOUBLE: ' . $this->_xh['ac']);
+                        $this->_xh['value'] = 'ERROR_NON_NUMERIC_FOUND';
                     } else {
-                        $this->getLogger()->errorLog('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'];
+                        // it's ok, add it on
+                        $this->_xh['value'] = (double)$this->_xh['ac'];
                     }
-                    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 {
+                    // we have an I4/I8/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!
+                        $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': non numeric value received in INT: ' . $this->_xh['ac']);
+                        $this->_xh['value'] = 'ERROR_NON_NUMERIC_FOUND';
                     } else {
-                        $this->getLogger()->errorLog('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;
+                        // it's ok, add it on
+                        $this->_xh['value'] = (int)$this->_xh['ac'];
                     }
-                // 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???
+                }
+                $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 {
+                    $this->getLogger()->errorLog('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 {
+                    $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': missing VALUE inside PARAM in received xml');
+                }
+                break;
+
+            case 'METHODNAME':
+                /// @todo why do we strip leading whitespace in method names, but not trailing whitespace?
+                $methodname = preg_replace('/^[\n\r\t ]+/', '', $this->_xh['ac']);
+                $this->_xh['method'] = $methodname;
+                // we allow the callback to f.e. give us back a mangled method name by manipulating $this
+                if (isset($this->callbacks['methodname'])) {
+                    call_user_func($this->callbacks['methodname'], $methodname, $this, $parser);
+                }
+                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
+                // Should we add an assert here for unreachable code? When an invalid element is found in xmlrpc_se,
+                //
+                break;
         }
     }
 
@@ -598,12 +677,13 @@ class XMLParser
     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;
-            }
+        if ($this->_xh['isf'] >= 2) {
+            return;
+        }
+
+        // "lookforvalue == 3" means that we've found an entire value and should discard any further character data
+        if ($this->_xh['lv'] != 3) {
+            $this->_xh['ac'] .= $data;
         }
     }
 
@@ -619,13 +699,13 @@ class XMLParser
     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;
-            }
+        if ($this->_xh['isf'] >= 2) {
+            return;
         }
 
-        //return true;
+        if (substr($data, 0, 1) == '&' && substr($data, -1, 1) == ';') {
+            $this->_xh['ac'] .= $data;
+        }
     }
 
     /**
index badb36e..20bf0a9 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace PhpXmlRpc;
 
+use PhpXmlRpc\Exception\PhpXmlrpcException;
 use PhpXmlRpc\Helper\Charset;
 use PhpXmlRpc\Helper\Logger;
 use PhpXmlRpc\Helper\XMLParser;
@@ -17,8 +18,8 @@ class Server
 
     /**
      * @var string
-     * 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'.
+     * Defines how functions in $dmap will be invoked: either using an xml-rpc Request object or plain php values.
+     * Valid strings are 'xmlrpcvals', 'phpvals' or 'epivals' (only for use by polyfill-xmlrpc).
      *
      * @todo create class constants for these
      */
@@ -27,7 +28,7 @@ class Server
     /**
      * @var array
      * 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'.
+     * when the functions_parameters_type member is set to 'phpvals'.
      * @see Encoder::encode for a list of values
      */
     public $phpvals_encoding_options = array('auto_dates');
@@ -35,13 +36,17 @@ class Server
     /**
      * @var int
      * 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
+     * Valid values:
+     * 0 =
+     * 1 =
+     * 2 =
+     * 3 =
      */
     public $debug = 1;
 
     /**
      * @var int
-     * Controls behaviour of server when the invoked user function throws an exception:
+     * Controls behaviour of server when the invoked method-handler function throws an exception (within the `execute` method):
      * 0 = catch it and return an 'internal error' 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
@@ -169,7 +174,7 @@ class Server
      *                             - docstring (optional)
      *                             - signature (array, optional)
      *                             - signature_docs (array, optional)
-     *                             - parameters_type (string, optional) - currently broken
+     *                             - parameters_type (string, optional)
      * @param boolean $serviceNow set to false to prevent the server from running upon construction
      */
     public function __construct($dispatchMap = null, $serviceNow = true)
@@ -303,15 +308,17 @@ class Server
             $this->debugmsg("+++GOT+++\n" . $data . "\n+++END+++");
         }
 
-        $r = $this->parseRequestHeaders($data, $reqCharset, $respCharset, $respEncoding);
-        if (!$r) {
+        $resp = $this->parseRequestHeaders($data, $reqCharset, $respCharset, $respEncoding);
+        if (!$resp) {
             // this actually executes the request
-            $r = $this->parseRequest($data, $reqCharset);
+            $resp = $this->parseRequest($data, $reqCharset);
 
-            // save full body of request into response, for more debugging usages.
-            // Note that this is the _request_ data, not the response's own data, unlike what happens client-side
-            /// @todo try to move this injection to the resp. constructor or use a non-deprecated access method
-            $r->raw_data = $rawData;
+            // save full body of request into response, for debugging purposes.
+            // NB: this is the _request_ data, not the response's own data, unlike what happens client-side
+            /// @todo try to move this injection to the resp. constructor or use a non-deprecated access method. Or, even
+            ///       better: just avoid setting this, and set debug info of the received http request in the request
+            ///       object instead? It's not like the developer misses access to _SERVER, _COOKIES though...
+            $resp->raw_data = $rawData;
         }
 
         if ($this->debug > 2 && static::$_xmlrpcs_occurred_errors != '') {
@@ -324,11 +331,11 @@ class Server
             $payload = $payload . $this->serializeDebug($respCharset);
         }
 
-        // Do not create response serialization if it has already happened. Helps building json magic
-        if (empty($r->payload)) {
-            $r->serialize($respCharset);
+        // Do not create response serialization if it has already happened. Helps to build json magic
+        if (empty($resp->payload)) {
+            $resp->serialize($respCharset);
         }
-        $payload = $payload . $r->payload;
+        $payload = $payload . $resp->payload;
 
         if ($returnPayload) {
             return $payload;
@@ -337,7 +344,7 @@ class Server
         // 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);
+            header('Content-Type: ' . $resp->content_type);
             // we do not know if client actually told us an accepted charset, but if it did we have to tell it what we did
             header("Vary: Accept-Charset");
 
@@ -368,8 +375,8 @@ class Server
 
         print $payload;
 
-        // return request, in case subclasses want it
-        return $r;
+        // return response, in case subclasses want it
+        return $resp;
     }
 
     /**
@@ -383,12 +390,12 @@ class Server
      * @param string $doc method documentation
      * @param array[] $sigDoc the array of valid method signatures docs, following the format of $sig but with
      *                        descriptions instead of types (one string for return type, one per param)
+     * @param string $parametersType to allow single method handlers to receive php values instead of a Request, or vice-versa
      * @return void
      *
      * @todo raise a warning if the user tries to register a 'system.' method
-     * @todo allow setting parameters_type
      */
-    public function add_to_map($methodName, $function, $sig = null, $doc = false, $sigDoc = false)
+    public function add_to_map($methodName, $function, $sig = null, $doc = false, $sigDoc = false, $parametersType = false)
     {
         $this->dmap[$methodName] = array(
             'function' => $function,
@@ -400,6 +407,9 @@ class Server
         if ($sigDoc) {
             $this->dmap[$methodName]['signature_docs'] = $sigDoc;
         }
+        if ($parametersType) {
+            $this->dmap[$methodName]['parameters_type'] = $parametersType;
+        }
     }
 
     /**
@@ -558,8 +568,8 @@ class Server
     }
 
     /**
-     * Parse an xml chunk containing an xmlrpc request and execute the corresponding
-     * php function registered with the server.
+     * Parse an xml chunk containing an xml-rpc request and execute the corresponding php function registered with the
+     * server.
      * @internal this function will become protected in the future
      *
      * @param string $data the xml request
@@ -605,27 +615,33 @@ class Server
         } else {
             $options = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
         }
+        // register a callback with the xml parser for when it finds the method name
+        $options['methodname_callback'] = array($this, 'methodNameCallback');
 
         $xmlRpcParser = $this->getParser();
-        $xmlRpcParser->parse($data, $this->functions_parameters_type, XMLParser::ACCEPT_REQUEST, $options);
+        try {
+            $xmlRpcParser->parse($data, $this->functions_parameters_type, XMLParser::ACCEPT_REQUEST, $options);
+        } catch (PhpXmlrpcException $e) {
+            return new Response(0, $e->getCode(), $e->getMessage());
+        }
+
+/// @todo handle the (unlikely) case of _xh['isf'] = 4
         if ($xmlRpcParser->_xh['isf'] > 2) {
             // (BC) we return XML error as a faultCode
             preg_match('/^XML error ([0-9]+)/', $xmlRpcParser->_xh['isf_reason'], $matches);
-            $r = new Response(
+            return new Response(
                 0,
                 PhpXmlRpc::$xmlrpcerrxml + (int)$matches[1],
                 $xmlRpcParser->_xh['isf_reason']);
         } elseif ($xmlRpcParser->_xh['isf']) {
-            $r = new Response(
+            return new Response(
                 0,
                 PhpXmlRpc::$xmlrpcerr['invalid_request'],
                 PhpXmlRpc::$xmlrpcstr['invalid_request'] . ' ' . $xmlRpcParser->_xh['isf_reason']);
         } else {
-            // small layering violation in favor of speed and memory usage:
-            // we should allow the 'execute' method handle this, but in the
-            // most common scenario (xmlrpc values type server with some methods
-            // registered as phpvals) that would mean a useless encode+decode pass
-            /// @bug when parameters_type is set in the method, we still get full-fledged Value objects
+            // 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'] != 'xmlrpcvals')
@@ -634,7 +650,8 @@ class Server
                 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']);
+
+                return $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']);
@@ -646,19 +663,18 @@ class Server
                 if ($this->debug > 1) {
                     $this->debugmsg("\n+++PARSED+++\n" . var_export($req, true) . "\n+++END+++");
                 }
-                $r = $this->execute($req);
+
+                return $this->execute($req);
             }
         }
-
-        return $r;
     }
 
     /**
      * Execute a method invoked by the client, checking parameters used.
      *
      * @param Request|string $req either a Request obj or a method name
-     * @param mixed[] $params array with method parameters as php types (only if m is method name)
-     * @param string[] $paramTypes array with xmlrpc types of method parameters (only if m is method name)
+     * @param mixed[] $params array with method parameters as php types (only if $req is method name)
+     * @param string[] $paramTypes array with xmlrpc types of method parameters (only if $req is method name)
      * @return Response
      *
      * @throws \Exception in case the executed method does throw an exception (and depending on server configuration)
@@ -669,14 +685,15 @@ class Server
         static::$_xmlrpc_debuginfo = '';
 
         if (is_object($req)) {
-            $methName = $req->method();
+            $methodName = $req->method();
         } else {
-            $methName = $req;
+            $methodName = $req;
         }
-        $sysCall = $this->isSyscall($methName);
+
+        $sysCall = $this->isSyscall($methodName);
         $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
 
-        if (!isset($dmap[$methName]['function'])) {
+        if (!isset($dmap[$methodName]['function'])) {
             // No such method
             return new Response(0,
                 PhpXmlRpc::$xmlrpcerr['unknown_method'],
@@ -684,8 +701,8 @@ class Server
         }
 
         // Check signature
-        if (isset($dmap[$methName]['signature'])) {
-            $sig = $dmap[$methName]['signature'];
+        if (isset($dmap[$methodName]['signature'])) {
+            $sig = $dmap[$methodName]['signature'];
             if (is_object($req)) {
                 list($ok, $errStr) = $this->verifySignature($req, $sig);
             } else {
@@ -701,12 +718,14 @@ class Server
             }
         }
 
-        $func = $dmap[$methName]['function'];
+        $func = $dmap[$methodName]['function'];
+
         // let the 'class::function' syntax be accepted in dispatch maps
         if (is_string($func) && strpos($func, '::')) {
             $func = explode('::', $func);
         }
 
+        // build string representation of function 'name'
         if (is_array($func)) {
             if (is_object($func[0])) {
                 $funcName = get_class($func[0]) . '->' . $func[1];
@@ -729,8 +748,8 @@ class Server
             );
         }
 
-        // If debug level is 3, we should catch all errors generated during
-        // processing of user function, and log them as part of response
+        // 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'));
         }
@@ -738,6 +757,7 @@ class Server
         try {
             // Allow mixed-convention servers
             if (is_object($req)) {
+                // call an 'xml-rpc aware' function
                 if ($sysCall) {
                     $r = call_user_func($func, $this, $req);
                 } else {
@@ -763,14 +783,13 @@ class Server
                 } 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 error response
+                        $r = call_user_func_array($func, array($methodName, $params, $this->user_data));
+                        // mimic EPI behaviour: if we get an array that looks like an error, make it an error response
                         if (is_array($r) && array_key_exists('faultCode', $r) && array_key_exists('faultString', $r)) {
                             $r = new 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
+                            // functions using EPI api should NOT return resp objects, so make sure we encode the
+                            // return type correctly
                             $encoder = new Encoder();
                             $r = new Response($encoder->encode($r, array('extension_api')));
                         }
@@ -780,7 +799,8 @@ class Server
                 }
                 // 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???
+                    // q: what should we assume here about automatic encoding of datetimes and php classes instances?
+                    // a: let the user decide
                     $encoder = new Encoder();
                     $r = new Response($encoder->encode($r, $this->phpvals_encoding_options));
                 }
@@ -832,9 +852,10 @@ class Server
                     $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
+            // 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 {
@@ -845,6 +866,51 @@ class Server
         return $r;
     }
 
+    /**
+     * Registered as callback for when the XMLParser has found the name of the method to execute.
+     * Handling that early allows to 1. stop parsing the rest of the xml if there is no such method registered, and
+     * 2. tweak the type of data that the parser will return, in case the server uses mixed-calling-convention
+     *
+     * @internal
+     * @param $methodName
+     * @param XMLParser $xmlParser
+     * @param resource $parser
+     * @return void
+     * @throws PhpXmlrpcException
+     *
+     * @todo feature creep - we could validate here that the method in the dispatch map is valid, but that would mean
+     *       dirtying a lot the logic, as we would have back to both parseRequest() and execute() methods the info
+     *       about the matched method handler, in order to avoid doing the work twice...
+     */
+    public function methodNameCallback($methodName, $xmlParser, $parser)
+    {
+        $sysCall = $this->isSyscall($methodName);
+        $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap;
+
+        if (!isset($dmap[$methodName]['function'])) {
+            // No such method
+            throw new PhpXmlrpcException(0, PhpXmlRpc::$xmlrpcstr['unknown_method'], PhpXmlRpc::$xmlrpcerr['unknown_method']);
+        }
+
+        // alter on-the-fly the config of the xml parser if needed
+        if (isset($dmap[$methodName]['parameters_type']) &&
+            $dmap[$methodName]['parameters_type'] != $this->functions_parameters_type) {
+            /// @todo this should be done by a method of the XMLParser
+            switch ($dmap[$methodName]['parameters_type']) {
+                case XMLParser::RETURN_PHP:
+                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast');
+                    break;
+                case XMLParser::RETURN_EPIVALS:
+                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_epi');
+                    break;
+                /// @todo log a warning on unsupported return type
+                case XMLParser::RETURN_XMLRPCVALS:
+                default:
+                    xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee');
+            }
+        }
+    }
+
     /**
      * Add a string to the 'internal debug message' (separate from 'user debug message').
      *
@@ -878,6 +944,7 @@ class Server
         return (strpos($methName, "system.") === 0);
     }
 
+
     /**
      * @return array[]
      */