WIP - more fixes
[plcapi.git] / src / Server.php
1 <?php
2
3 namespace PhpXmlRpc;
4
5 use PhpXmlRpc\Helper\XMLParser;
6 use PhpXmlRpc\Helper\Charset;
7
8 /**
9 * Error handler used to track errors that occur during server-side execution of PHP code.
10 * This allows to report back to the client whether an internal error has occurred or not
11 * using an xmlrpc response object, instead of letting the client deal with the html junk
12 * that a PHP execution error on the server generally entails.
13 *
14 * NB: in fact a user defined error handler can only handle WARNING, NOTICE and USER_* errors.
15 *
16 */
17 function _xmlrpcs_errorHandler($errcode, $errstring, $filename=null, $lineno=null, $context=null)
18 {
19     // obey the @ protocol
20     if (error_reporting() == 0)
21         return;
22
23     //if($errcode != E_NOTICE && $errcode != E_WARNING && $errcode != E_USER_NOTICE && $errcode != E_USER_WARNING)
24     if($errcode != E_STRICT)
25     {
26         \PhpXmlRpc\Server::error_occurred($errstring);
27     }
28     // Try to avoid as much as possible disruption to the previous error handling
29     // mechanism in place
30     if($GLOBALS['_xmlrpcs_prev_ehandler'] == '')
31     {
32         // The previous error handler was the default: all we should do is log error
33         // to the default error log (if level high enough)
34         if(ini_get('log_errors') && (intval(ini_get('error_reporting')) & $errcode))
35         {
36             error_log($errstring);
37         }
38     }
39     else
40     {
41         // Pass control on to previous error handler, trying to avoid loops...
42         if($GLOBALS['_xmlrpcs_prev_ehandler'] != '_xmlrpcs_errorHandler')
43         {
44             // NB: this code will NOT work on php < 4.0.2: only 2 params were used for error handlers
45             if(is_array($GLOBALS['_xmlrpcs_prev_ehandler']))
46             {
47                 // the following works both with static class methods and plain object methods as error handler
48                 call_user_func_array($GLOBALS['_xmlrpcs_prev_ehandler'], array($errcode, $errstring, $filename, $lineno, $context));
49             }
50             else
51             {
52                 $GLOBALS['_xmlrpcs_prev_ehandler']($errcode, $errstring, $filename, $lineno, $context);
53             }
54         }
55     }
56 }
57
58
59 class Server
60 {
61     /**
62     * Array defining php functions exposed as xmlrpc methods by this server
63     */
64     protected $dmap=array();
65     /**
66     * Defines how functions in dmap will be invoked: either using an xmlrpc msg object
67     * or plain php values.
68     * valid strings are 'xmlrpcvals', 'phpvals' or 'epivals'
69     */
70     var $functions_parameters_type='xmlrpcvals';
71     /**
72     * Option used for fine-tuning the encoding the php values returned from
73     * functions registered in the dispatch map when the functions_parameters_types
74     * member is set to 'phpvals'
75     * @see php_xmlrpc_encode for a list of values
76     */
77     var $phpvals_encoding_options = array( 'auto_dates' );
78     /// 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
79     var $debug = 1;
80     /**
81     * Controls behaviour of server when invoked user function throws an exception:
82     * 0 = catch it and return an 'internal error' xmlrpc response (default)
83     * 1 = catch it and return an xmlrpc response with the error corresponding to the exception
84     * 2 = allow the exception to float to the upper layers
85     */
86     var $exception_handling = 0;
87     /**
88     * When set to true, it will enable HTTP compression of the response, in case
89     * the client has declared its support for compression in the request.
90     */
91     var $compress_response = false;
92     /**
93     * List of http compression methods accepted by the server for requests.
94     * NB: PHP supports deflate, gzip compressions out of the box if compiled w. zlib
95     */
96     var $accepted_compression = array();
97     /// shall we serve calls to system.* methods?
98     var $allow_system_funcs = true;
99     /// list of charset encodings natively accepted for requests
100     var $accepted_charset_encodings = array();
101     /**
102     * charset encoding to be used for response.
103     * NB: if we can, we will convert the generated response from internal_encoding to the intended one.
104     * can be: a supported xml encoding (only UTF-8 and ISO-8859-1 at present, unless mbstring is enabled),
105     * null (leave unspecified in response, convert output stream to US_ASCII),
106     * 'default' (use xmlrpc library default as specified in xmlrpc.inc, convert output stream if needed),
107     * 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).
108     * NB: pretty dangerous if you accept every charset and do not have mbstring enabled)
109     */
110     var $response_charset_encoding = '';
111     /**
112     * Storage for internal debug info
113     */
114     protected $debug_info = '';
115     /**
116     * Extra data passed at runtime to method handling functions. Used only by EPI layer
117     */
118     var $user_data = null;
119
120     static protected $_xmlrpc_debuginfo = '';
121     static protected $_xmlrpcs_occurred_errors = '';
122     static $_xmlrpcs_prev_ehandler = '';
123
124     /**
125     * @param array $dispmap the dispatch map with definition of exposed services
126     * @param boolean $servicenow set to false to prevent the server from running upon construction
127     */
128     function __construct($dispMap=null, $serviceNow=true)
129     {
130         // if ZLIB is enabled, let the server by default accept compressed requests,
131         // and compress responses sent to clients that support them
132         if(function_exists('gzinflate'))
133         {
134             $this->accepted_compression = array('gzip', 'deflate');
135             $this->compress_response = true;
136         }
137
138         // by default the xml parser can support these 3 charset encodings
139         $this->accepted_charset_encodings = array('UTF-8', 'ISO-8859-1', 'US-ASCII');
140
141         // dispMap is a dispatch array of methods
142         // mapped to function names and signatures
143         // if a method
144         // doesn't appear in the map then an unknown
145         // method error is generated
146         /* milosch - changed to make passing dispMap optional.
147             * instead, you can use the class add_to_map() function
148             * to add functions manually (borrowed from SOAPX4)
149             */
150         if($dispMap)
151         {
152             $this->dmap = $dispMap;
153             if($serviceNow)
154             {
155                 $this->service();
156             }
157         }
158     }
159
160     /**
161     * Set debug level of server.
162     * @param integer $in debug lvl: determines info added to xmlrpc responses (as xml comments)
163     * 0 = no debug info,
164     * 1 = msgs set from user with debugmsg(),
165     * 2 = add complete xmlrpc request (headers and body),
166     * 3 = add also all processing warnings happened during method processing
167     * (NB: this involves setting a custom error handler, and might interfere
168     * with the standard processing of the php function exposed as method. In
169     * particular, triggering an USER_ERROR level error will not halt script
170     * execution anymore, but just end up logged in the xmlrpc response)
171     * Note that info added at level 2 and 3 will be base64 encoded
172     */
173     function setDebug($in)
174     {
175         $this->debug=$in;
176     }
177
178     /**
179      * Add a string to the debug info that can be later serialized by the server
180      * as part of the response message.
181      * Note that for best compatibility, the debug string should be encoded using
182      * the PhpXmlRpc::$xmlrpc_internalencoding character set.
183      * @param string $m
184      * @access public
185      */
186     public static function xmlrpc_debugmsg($m)
187     {
188         static::$_xmlrpc_debuginfo .= $m . "\n";
189     }
190
191     public static function error_occurred($m)
192     {
193         static::$_xmlrpcs_occurred_errors .= $m . "\n";
194     }
195
196     /**
197     * Return a string with the serialized representation of all debug info
198     * @param string $charset_encoding the target charset encoding for the serialization
199     * @return string an XML comment (or two)
200     */
201     function serializeDebug($charset_encoding='')
202     {
203         // Tough encoding problem: which internal charset should we assume for debug info?
204         // It might contain a copy of raw data received from client, ie with unknown encoding,
205         // intermixed with php generated data and user generated data...
206         // so we split it: system debug is base 64 encoded,
207         // user debug info should be encoded by the end user using the INTERNAL_ENCODING
208         $out = '';
209         if ($this->debug_info != '')
210         {
211             $out .= "<!-- SERVER DEBUG INFO (BASE64 ENCODED):\n".base64_encode($this->debug_info)."\n-->\n";
212         }
213         if( static::$_xmlrpc_debuginfo!='')
214         {
215
216             $out .= "<!-- DEBUG INFO:\n" . Charset::instance()->encode_entities(str_replace('--', '_-', static::$_xmlrpc_debuginfo), PhpXmlRpc::$xmlrpc_internalencoding, $charset_encoding) . "\n-->\n";
217             // NB: a better solution MIGHT be to use CDATA, but we need to insert it
218             // into return payload AFTER the beginning tag
219             //$out .= "<![CDATA[ DEBUG INFO:\n\n" . str_replace(']]>', ']_]_>', static::$_xmlrpc_debuginfo) . "\n]]>\n";
220         }
221         return $out;
222     }
223
224     /**
225      * Execute the xmlrpc request, printing the response
226      * @param string $data the request body. If null, the http POST request will be examined
227      * @param bool $return_payload When true, return the response but do not echo it or any http header
228      * @return xmlrpcresp the response object (usually not used by caller...)
229      */
230     function service($data=null, $return_payload=false)
231     {
232         if ($data === null)
233         {
234             // workaround for a known bug in php ver. 5.2.2 that broke $HTTP_RAW_POST_DATA
235             $data = file_get_contents('php://input');
236         }
237         $raw_data = $data;
238
239         // reset internal debug info
240         $this->debug_info = '';
241
242         // Echo back what we received, before parsing it
243         if($this->debug > 1)
244         {
245             $this->debugmsg("+++GOT+++\n" . $data . "\n+++END+++");
246         }
247
248         $r = $this->parseRequestHeaders($data, $req_charset, $resp_charset, $resp_encoding);
249         if (!$r)
250         {
251             $r=$this->parseRequest($data, $req_charset);
252         }
253
254         // save full body of request into response, for more debugging usages
255         $r->raw_data = $raw_data;
256
257         if($this->debug > 2 && static::$_xmlrpcs_occurred_errors)
258         {
259             $this->debugmsg("+++PROCESSING ERRORS AND WARNINGS+++\n" .
260                 static::$_xmlrpcs_occurred_errors . "+++END+++");
261         }
262
263         $payload=$this->xml_header($resp_charset);
264         if($this->debug > 0)
265         {
266             $payload = $payload . $this->serializeDebug($resp_charset);
267         }
268
269         // G. Giunta 2006-01-27: do not create response serialization if it has
270         // already happened. Helps building json magic
271         if (empty($r->payload))
272         {
273             $r->serialize($resp_charset);
274         }
275         $payload = $payload . $r->payload;
276
277         if ($return_payload)
278         {
279             return $payload;
280         }
281
282         // if we get a warning/error that has output some text before here, then we cannot
283         // add a new header. We cannot say we are sending xml, either...
284         if(!headers_sent())
285         {
286             header('Content-Type: '.$r->content_type);
287             // we do not know if client actually told us an accepted charset, but if he did
288             // we have to tell him what we did
289             header("Vary: Accept-Charset");
290
291             // http compression of output: only
292             // if we can do it, and we want to do it, and client asked us to,
293             // and php ini settings do not force it already
294             $php_no_self_compress = !ini_get('zlib.output_compression') && (ini_get('output_handler') != 'ob_gzhandler');
295             if($this->compress_response && function_exists('gzencode') && $resp_encoding != ''
296                 && $php_no_self_compress)
297             {
298                 if(strpos($resp_encoding, 'gzip') !== false)
299                 {
300                     $payload = gzencode($payload);
301                     header("Content-Encoding: gzip");
302                     header("Vary: Accept-Encoding");
303                 }
304                 elseif (strpos($resp_encoding, 'deflate') !== false)
305                 {
306                     $payload = gzcompress($payload);
307                     header("Content-Encoding: deflate");
308                     header("Vary: Accept-Encoding");
309                 }
310             }
311
312             // do not output content-length header if php is compressing output for us:
313             // it will mess up measurements
314             if($php_no_self_compress)
315             {
316                 header('Content-Length: ' . (int)strlen($payload));
317             }
318         }
319         else
320         {
321             error_log('XML-RPC: '.__METHOD__.': http headers already sent before response is fully generated. Check for php warning or error messages');
322         }
323
324         print $payload;
325
326         // return request, in case subclasses want it
327         return $r;
328     }
329
330     /**
331     * Add a method to the dispatch map
332     * @param string $methodname the name with which the method will be made available
333     * @param string $function the php function that will get invoked
334     * @param array $sig the array of valid method signatures
335     * @param string $doc method documentation
336     * @param array $sigdoc the array of valid method signatures docs (one string per param, one for return type)
337     */
338     function add_to_map($methodname,$function,$sig=null,$doc=false,$sigdoc=false)
339     {
340         $this->dmap[$methodname] = array(
341             'function' => $function,
342             'docstring' => $doc
343         );
344         if ($sig)
345         {
346             $this->dmap[$methodname]['signature'] = $sig;
347         }
348         if ($sigdoc)
349         {
350             $this->dmap[$methodname]['signature_docs'] = $sigdoc;
351         }
352     }
353
354     /**
355     * Verify type and number of parameters received against a list of known signatures
356     * @param array $in array of either xmlrpcval objects or xmlrpc type definitions
357     * @param array $sig array of known signatures to match against
358     * @return array
359     */
360     protected function verifySignature($in, $sig)
361     {
362         // check each possible signature in turn
363         if (is_object($in))
364         {
365             $numParams = $in->getNumParams();
366         }
367         else
368         {
369             $numParams = count($in);
370         }
371         foreach($sig as $cursig)
372         {
373             if(count($cursig)==$numParams+1)
374             {
375                 $itsOK=1;
376                 for($n=0; $n<$numParams; $n++)
377                 {
378                     if (is_object($in))
379                     {
380                         $p=$in->getParam($n);
381                         if($p->kindOf() == 'scalar')
382                         {
383                             $pt=$p->scalartyp();
384                         }
385                         else
386                         {
387                             $pt=$p->kindOf();
388                         }
389                     }
390                     else
391                     {
392                         $pt= $in[$n] == 'i4' ? 'int' : strtolower($in[$n]); // dispatch maps never use i4...
393                     }
394
395                     // param index is $n+1, as first member of sig is return type
396                     if($pt != $cursig[$n+1] && $cursig[$n+1] != Value::$xmlrpcValue)
397                     {
398                         $itsOK=0;
399                         $pno=$n+1;
400                         $wanted=$cursig[$n+1];
401                         $got=$pt;
402                         break;
403                     }
404                 }
405                 if($itsOK)
406                 {
407                     return array(1,'');
408                 }
409             }
410         }
411         if(isset($wanted))
412         {
413             return array(0, "Wanted ${wanted}, got ${got} at param ${pno}");
414         }
415         else
416         {
417             return array(0, "No method signature matches number of parameters");
418         }
419     }
420
421     /**
422     * Parse http headers received along with xmlrpc request. If needed, inflate request
423     * @return mixed null on success or an xmlrpcresp
424     */
425     protected function parseRequestHeaders(&$data, &$req_encoding, &$resp_encoding, &$resp_compression)
426     {
427         // check if $_SERVER is populated: it might have been disabled via ini file
428         // (this is true even when in CLI mode)
429         if (count($_SERVER) == 0)
430         {
431             error_log('XML-RPC: '.__METHOD__.': cannot parse request headers as $_SERVER is not populated');
432         }
433
434         if($this->debug > 1)
435         {
436             if(function_exists('getallheaders'))
437             {
438                 $this->debugmsg(''); // empty line
439                 foreach(getallheaders() as $name => $val)
440                 {
441                     $this->debugmsg("HEADER: $name: $val");
442                 }
443             }
444
445         }
446
447         if(isset($_SERVER['HTTP_CONTENT_ENCODING']))
448         {
449             $content_encoding = str_replace('x-', '', $_SERVER['HTTP_CONTENT_ENCODING']);
450         }
451         else
452         {
453             $content_encoding = '';
454         }
455
456         // check if request body has been compressed and decompress it
457         if($content_encoding != '' && strlen($data))
458         {
459             if($content_encoding == 'deflate' || $content_encoding == 'gzip')
460             {
461                 // if decoding works, use it. else assume data wasn't gzencoded
462                 if(function_exists('gzinflate') && in_array($content_encoding, $this->accepted_compression))
463                 {
464                     if($content_encoding == 'deflate' && $degzdata = @gzuncompress($data))
465                     {
466                         $data = $degzdata;
467                         if($this->debug > 1)
468                         {
469                             $this->debugmsg("\n+++INFLATED REQUEST+++[".strlen($data)." chars]+++\n" . $data . "\n+++END+++");
470                         }
471                     }
472                     elseif($content_encoding == 'gzip' && $degzdata = @gzinflate(substr($data, 10)))
473                     {
474                         $data = $degzdata;
475                         if($this->debug > 1)
476                             $this->debugmsg("+++INFLATED REQUEST+++[".strlen($data)." chars]+++\n" . $data . "\n+++END+++");
477                     }
478                     else
479                     {
480                         $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_decompress_fail'], PhpXmlRpc::$xmlrpcstr['server_decompress_fail']);
481                         return $r;
482                     }
483                 }
484                 else
485                 {
486                     //error_log('The server sent deflated data. Your php install must have the Zlib extension compiled in to support this.');
487                     $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_cannot_decompress'], PhpXmlRpc::$xmlrpcstr['server_cannot_decompress']);
488                     return $r;
489                 }
490             }
491         }
492
493         // check if client specified accepted charsets, and if we know how to fulfill
494         // the request
495         if ($this->response_charset_encoding == 'auto')
496         {
497             $resp_encoding = '';
498             if (isset($_SERVER['HTTP_ACCEPT_CHARSET']))
499             {
500                 // here we should check if we can match the client-requested encoding
501                 // with the encodings we know we can generate.
502                 /// @todo we should parse q=0.x preferences instead of getting first charset specified...
503                 $client_accepted_charsets = explode(',', strtoupper($_SERVER['HTTP_ACCEPT_CHARSET']));
504                 // Give preference to internal encoding
505                 $known_charsets = array(PhpXmlRpc::$xmlrpc_internalencoding, 'UTF-8', 'ISO-8859-1', 'US-ASCII');
506                 foreach ($known_charsets as $charset)
507                 {
508                     foreach ($client_accepted_charsets as $accepted)
509                         if (strpos($accepted, $charset) === 0)
510                         {
511                             $resp_encoding = $charset;
512                             break;
513                         }
514                     if ($resp_encoding)
515                         break;
516                 }
517             }
518         }
519         else
520         {
521             $resp_encoding = $this->response_charset_encoding;
522         }
523
524         if (isset($_SERVER['HTTP_ACCEPT_ENCODING']))
525         {
526             $resp_compression = $_SERVER['HTTP_ACCEPT_ENCODING'];
527         }
528         else
529         {
530             $resp_compression = '';
531         }
532
533         // 'guestimate' request encoding
534         /// @todo check if mbstring is enabled and automagic input conversion is on: it might mingle with this check???
535         $req_encoding = guess_encoding(isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : '',
536             $data);
537
538         return null;
539     }
540
541     /**
542     * Parse an xml chunk containing an xmlrpc request and execute the corresponding
543     * php function registered with the server
544     * @param string $data the xml request
545     * @param string $req_encoding (optional) the charset encoding of the xml request
546     * @return xmlrpcresp
547     */
548     public function parseRequest($data, $req_encoding='')
549     {
550         // 2005/05/07 commented and moved into caller function code
551         //if($data=='')
552         //{
553         //    $data=$GLOBALS['HTTP_RAW_POST_DATA'];
554         //}
555
556         // G. Giunta 2005/02/13: we do NOT expect to receive html entities
557         // so we do not try to convert them into xml character entities
558         //$data = xmlrpc_html_entity_xlate($data);
559
560         // decompose incoming XML into request structure
561         if ($req_encoding != '')
562         {
563             if (!in_array($req_encoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII')))
564             // the following code might be better for mb_string enabled installs, but
565             // makes the lib about 200% slower...
566             //if (!is_valid_charset($req_encoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII')))
567             {
568                 error_log('XML-RPC: '.__METHOD__.': invalid charset encoding of received request: '.$req_encoding);
569                 $req_encoding = PhpXmlRpc::$xmlrpc_defencoding;
570             }
571             /// @BUG this will fail on PHP 5 if charset is not specified in the xml prologue,
572             // the encoding is not UTF8 and there are non-ascii chars in the text...
573             /// @todo use an empty string for php 5 ???
574             $parser = xml_parser_create($req_encoding);
575         }
576         else
577         {
578             $parser = xml_parser_create();
579         }
580
581         xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, true);
582         // G. Giunta 2005/02/13: PHP internally uses ISO-8859-1, so we have to tell
583         // the xml parser to give us back data in the expected charset
584         // What if internal encoding is not in one of the 3 allowed?
585         // we use the broadest one, ie. utf8
586         // This allows to send data which is native in various charset,
587         // by extending xmlrpc_encode_entities() and setting xmlrpc_internalencoding
588         if (!in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII')))
589         {
590             xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
591         }
592         else
593         {
594             xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, PhpXmlRpc::$xmlrpc_internalencoding);
595         }
596
597         $xmlRpcParser = new XMLParser();
598         xml_set_object($parser, $xmlRpcParser);
599
600         if ($this->functions_parameters_type != 'xmlrpcvals')
601             xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast');
602         else
603             xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee');
604         xml_set_character_data_handler($parser, 'xmlrpc_cd');
605         xml_set_default_handler($parser, 'xmlrpc_dh');
606         if(!xml_parse($parser, $data, 1))
607         {
608             // return XML error as a faultCode
609             $r=new Response(0,
610                 PhpXmlRpc::$xmlrpcerrxml+xml_get_error_code($parser),
611             sprintf('XML error: %s at line %d, column %d',
612                 xml_error_string(xml_get_error_code($parser)),
613                 xml_get_current_line_number($parser), xml_get_current_column_number($parser)));
614             xml_parser_free($parser);
615         }
616         elseif ($xmlRpcParser->_xh['isf'])
617         {
618             xml_parser_free($parser);
619             $r=new Response(0,
620                 PhpXmlRpc::$xmlrpcerr['invalid_request'],
621                 PhpXmlRpc::$xmlrpcstr['invalid_request'] . ' ' . $xmlRpcParser->_xh['isf_reason']);
622         }
623         else
624         {
625             xml_parser_free($parser);
626             // small layering violation in favor of speed and memory usage:
627             // we should allow the 'execute' method handle this, but in the
628             // most common scenario (xmlrpcvals type server with some methods
629             // registered as phpvals) that would mean a useless encode+decode pass
630             if ($this->functions_parameters_type != 'xmlrpcvals' || (isset($this->dmap[$xmlRpcParser->_xh['method']]['parameters_type']) && ($this->dmap[$xmlRpcParser->_xh['method']]['parameters_type'] == 'phpvals')))
631             {
632                 if($this->debug > 1)
633                 {
634                     $this->debugmsg("\n+++PARSED+++\n".var_export($xmlRpcParser->_xh['params'], true)."\n+++END+++");
635                 }
636                 $r = $this->execute($xmlRpcParser->_xh['method'], $xmlRpcParser->_xh['params'], $xmlRpcParser->_xh['pt']);
637             }
638             else
639             {
640                 // build a Request object with data parsed from xml
641                 $m=new Request($xmlRpcParser->_xh['method']);
642                 // now add parameters in
643                 for($i=0; $i<count($xmlRpcParser->_xh['params']); $i++)
644                 {
645                     $m->addParam($xmlRpcParser->_xh['params'][$i]);
646                 }
647
648                 if($this->debug > 1)
649                 {
650                     $this->debugmsg("\n+++PARSED+++\n".var_export($m, true)."\n+++END+++");
651                 }
652                 $r = $this->execute($m);
653             }
654         }
655         return $r;
656     }
657
658     /**
659     * Execute a method invoked by the client, checking parameters used
660     * @param mixed $m either an xmlrpcmsg obj or a method name
661     * @param array $params array with method parameters as php types (if m is method name only)
662     * @param array $paramtypes array with xmlrpc types of method parameters (if m is method name only)
663     * @return xmlrpcresp
664     */
665     protected function execute($m, $params=null, $paramtypes=null)
666     {
667         if (is_object($m))
668         {
669             $methName = $m->method();
670         }
671         else
672         {
673             $methName = $m;
674         }
675         $sysCall = $this->allow_system_funcs && (strpos($methName, "system.") === 0);
676         $dmap = $sysCall ? $GLOBALS['_xmlrpcs_dmap'] : $this->dmap;
677
678         if(!isset($dmap[$methName]['function']))
679         {
680             // No such method
681             return new Response(0,
682                 PhpXmlRpc::$xmlrpcerr['unknown_method'],
683                 PhpXmlRpc::$xmlrpcstr['unknown_method']);
684         }
685
686         // Check signature
687         if(isset($dmap[$methName]['signature']))
688         {
689             $sig = $dmap[$methName]['signature'];
690             if (is_object($m))
691             {
692                 list($ok, $errstr) = $this->verifySignature($m, $sig);
693             }
694             else
695             {
696                 list($ok, $errstr) = $this->verifySignature($paramtypes, $sig);
697             }
698             if(!$ok)
699             {
700                 // Didn't match.
701                 return new Response(
702                     0,
703                     PhpXmlRpc::$xmlrpcerr['incorrect_params'],
704                     PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": ${errstr}"
705                 );
706             }
707         }
708
709         $func = $dmap[$methName]['function'];
710         // let the 'class::function' syntax be accepted in dispatch maps
711         if(is_string($func) && strpos($func, '::'))
712         {
713             $func = explode('::', $func);
714         }
715         // verify that function to be invoked is in fact callable
716         if(!is_callable($func))
717         {
718             error_log("XML-RPC: ".__METHOD__.": function $func registered as method handler is not callable");
719             return new Response(
720                 0,
721                 PhpXmlRpc::$xmlrpcerr['server_error'],
722                 PhpXmlRpc::$xmlrpcstr['server_error'] . ": no function matches method"
723             );
724         }
725
726         // If debug level is 3, we should catch all errors generated during
727         // processing of user function, and log them as part of response
728         if($this->debug > 2)
729         {
730             $GLOBALS['_xmlrpcs_prev_ehandler'] = set_error_handler('_xmlrpcs_errorHandler');
731         }
732         try
733         {
734             // Allow mixed-convention servers
735             if (is_object($m))
736             {
737                 if($sysCall)
738                 {
739                     $r = call_user_func($func, $this, $m);
740                 }
741                 else
742                 {
743                     $r = call_user_func($func, $m);
744                 }
745                 if (!is_a($r, 'xmlrpcresp'))
746                 {
747                     error_log("XML-RPC: ".__METHOD__.": function $func registered as method handler does not return an xmlrpcresp object");
748                     if (is_a($r, 'PhpXmlRpc\Value'))
749                     {
750                         $r = new Response($r);
751                     }
752                     else
753                     {
754                         $r = new Response(
755                             0,
756                             PhpXmlRpc::$xmlrpcerr['server_error'],
757                             PhpXmlRpc::$xmlrpcstr['server_error'] . ": function does not return xmlrpcresp object"
758                         );
759                     }
760                 }
761             }
762             else
763             {
764                 // call a 'plain php' function
765                 if($sysCall)
766                 {
767                     array_unshift($params, $this);
768                     $r = call_user_func_array($func, $params);
769                 }
770                 else
771                 {
772                     // 3rd API convention for method-handling functions: EPI-style
773                     if ($this->functions_parameters_type == 'epivals')
774                     {
775                         $r = call_user_func_array($func, array($methName, $params, $this->user_data));
776                         // mimic EPI behaviour: if we get an array that looks like an error, make it
777                         // an eror response
778                         if (is_array($r) && array_key_exists('faultCode', $r) && array_key_exists('faultString', $r))
779                         {
780                             $r = new Response(0, (integer)$r['faultCode'], (string)$r['faultString']);
781                         }
782                         else
783                         {
784                             // functions using EPI api should NOT return resp objects,
785                             // so make sure we encode the return type correctly
786                             $r = new Response(php_xmlrpc_encode($r, array('extension_api')));
787                         }
788                     }
789                     else
790                     {
791                         $r = call_user_func_array($func, $params);
792                     }
793                 }
794                 // the return type can be either an xmlrpcresp object or a plain php value...
795                 if (!is_a($r, 'xmlrpcresp'))
796                 {
797                     // what should we assume here about automatic encoding of datetimes
798                     // and php classes instances???
799                     $r = new Response(php_xmlrpc_encode($r, $this->phpvals_encoding_options));
800                 }
801             }
802         }
803         catch(Exception $e)
804         {
805             // (barring errors in the lib) an uncatched exception happened
806             // in the called function, we wrap it in a proper error-response
807             switch($this->exception_handling)
808             {
809                 case 2:
810                     throw $e;
811                     break;
812                 case 1:
813                     $r = new Response(0, $e->getCode(), $e->getMessage());
814                     break;
815                 default:
816                     $r = new Response(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']);
817             }
818         }
819         if($this->debug > 2)
820         {
821             // note: restore the error handler we found before calling the
822             // user func, even if it has been changed inside the func itself
823             if($GLOBALS['_xmlrpcs_prev_ehandler'])
824             {
825                 set_error_handler($GLOBALS['_xmlrpcs_prev_ehandler']);
826             }
827             else
828             {
829                 restore_error_handler();
830             }
831         }
832         return $r;
833     }
834
835     /**
836     * add a string to the 'internal debug message' (separate from 'user debug message')
837     * @param string $string
838     */
839     protected function debugmsg($string)
840     {
841         $this->debug_info .= $string."\n";
842     }
843
844     protected function xml_header($charset_encoding='')
845     {
846         if ($charset_encoding != '')
847         {
848             return "<?xml version=\"1.0\" encoding=\"$charset_encoding\"?" . ">\n";
849         }
850         else
851         {
852             return "<?xml version=\"1.0\"?" . ">\n";
853         }
854     }
855
856     /**
857     * A debugging routine: just echoes back the input packet as a string value
858     * DEPRECATED!
859     */
860     function echoInput()
861     {
862         $r=new Response(new Value( "'Aha said I: '" . $GLOBALS['HTTP_RAW_POST_DATA'], 'string'));
863         print $r->serialize();
864     }
865
866     /* Functions that implement system.XXX methods of xmlrpc servers */
867
868     protected function getSystemDispatchMap()
869     {
870         return array(
871             'system.listMethods' => array(
872                 'function' => 'PhpXmlRpc\Server::_xmlrpcs_listMethods',
873                 // listMethods: signature was either a string, or nothing.
874                 // The useless string variant has been removed
875                 'signature' => array(array(Value::$xmlrpcArray)),
876                 'docstring' => 'This method lists all the methods that the XML-RPC server knows how to dispatch',
877                 'signature_docs' => array(array('list of method names')),
878             ),
879             'system.methodHelp' => array(
880                 'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodHelp',
881                 'signature' => array(array(Value::$xmlrpcString, Value::$xmlrpcString)),
882                 'docstring' => 'Returns help text if defined for the method passed, otherwise returns an empty string',
883                 'signature_docs' => array(array('method description', 'name of the method to be described')),
884             ),
885             'system.methodSignature' => array(
886                 'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodSignature',
887                 'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcString)),
888                 '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)',
889                 'signature_docs' => array(array('list of known signatures, each sig being an array of xmlrpc type names', 'name of method to be described')),
890             ),
891             'system.multicall' => array(
892                 'function' => 'PhpXmlRpc\Server::_xmlrpcs_multicall',
893                 'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcArray)),
894                 'docstring' => 'Boxcar multiple RPC calls in one request. See http://www.xmlrpc.com/discuss/msgReader$1208 for details',
895                 '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"')),
896             ),
897             'system.getCapabilities' => array(
898                 'function' => 'PhpXmlRpc\Server::_xmlrpcs_getCapabilities',
899                 'signature' => array(array(Value::$xmlrpcStruct)),
900                 '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',
901                 'signature_docs' => array(array('list of capabilities, described as structs with a version number and url for the spec'))
902             )
903         );
904     }
905
906     public static function _xmlrpcs_getCapabilities($server, $m=null)
907     {
908         $outAr = array(
909             // xmlrpc spec: always supported
910             'xmlrpc' => new Value(array(
911                 'specUrl' => new Value('http://www.xmlrpc.com/spec', 'string'),
912                 'specVersion' => new Value(1, 'int')
913             ), 'struct'),
914             // if we support system.xxx functions, we always support multicall, too...
915             // Note that, as of 2006/09/17, the following URL does not respond anymore
916             'system.multicall' => new Value(array(
917                 'specUrl' => new Value('http://www.xmlrpc.com/discuss/msgReader$1208', 'string'),
918                 'specVersion' => new Value(1, 'int')
919             ), 'struct'),
920             // introspection: version 2! we support 'mixed', too
921             'introspection' => new Value(array(
922                 'specUrl' => new Value('http://phpxmlrpc.sourceforge.net/doc-2/ch10.html', 'string'),
923                 'specVersion' => new Value(2, 'int')
924             ), 'struct')
925         );
926
927         // NIL extension
928         if (PhpXmlRpc::$xmlrpc_null_extension) {
929             $outAr['nil'] = new Value(array(
930                 'specUrl' => new Value('http://www.ontosys.com/xml-rpc/extensions.php', 'string'),
931                 'specVersion' => new Value(1, 'int')
932             ), 'struct');
933         }
934         return new Response(new Value($outAr, 'struct'));
935     }
936
937     public static function _xmlrpcs_listMethods($server, $m=null) // if called in plain php values mode, second param is missing
938     {
939
940         $outAr=array();
941         foreach($server->dmap as $key => $val)
942         {
943             $outAr[]=new Value($key, 'string');
944         }
945         if($server->allow_system_funcs)
946         {
947             foreach($GLOBALS['_xmlrpcs_dmap'] as $key => $val)
948             {
949                 $outAr[]=new Value($key, 'string');
950             }
951         }
952         return new Response(new Value($outAr, 'array'));
953     }
954
955     public static function _xmlrpcs_methodSignature($server, $m)
956     {
957         // let accept as parameter both an xmlrpcval or string
958         if (is_object($m))
959         {
960             $methName=$m->getParam(0);
961             $methName=$methName->scalarval();
962         }
963         else
964         {
965             $methName=$m;
966         }
967         if(strpos($methName, "system.") === 0)
968         {
969             $dmap=$GLOBALS['_xmlrpcs_dmap']; $sysCall=1;
970         }
971         else
972         {
973             $dmap=$server->dmap; $sysCall=0;
974         }
975         if(isset($dmap[$methName]))
976         {
977             if(isset($dmap[$methName]['signature']))
978             {
979                 $sigs=array();
980                 foreach($dmap[$methName]['signature'] as $inSig)
981                 {
982                     $cursig=array();
983                     foreach($inSig as $sig)
984                     {
985                         $cursig[]=new Value($sig, 'string');
986                     }
987                     $sigs[]=new Value($cursig, 'array');
988                 }
989                 $r=new Response(new Value($sigs, 'array'));
990             }
991             else
992             {
993                 // NB: according to the official docs, we should be returning a
994                 // "none-array" here, which means not-an-array
995                 $r=new Response(new Value('undef', 'string'));
996             }
997         }
998         else
999         {
1000             $r=new Response(0,PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
1001         }
1002         return $r;
1003     }
1004
1005     public static function _xmlrpcs_methodHelp($server, $m)
1006     {
1007         // let accept as parameter both an xmlrpcval or string
1008         if (is_object($m))
1009         {
1010             $methName=$m->getParam(0);
1011             $methName=$methName->scalarval();
1012         }
1013         else
1014         {
1015             $methName=$m;
1016         }
1017         if(strpos($methName, "system.") === 0)
1018         {
1019             $dmap=$GLOBALS['_xmlrpcs_dmap']; $sysCall=1;
1020         }
1021         else
1022         {
1023             $dmap=$server->dmap; $sysCall=0;
1024         }
1025         if(isset($dmap[$methName]))
1026         {
1027             if(isset($dmap[$methName]['docstring']))
1028             {
1029                 $r=new Response(new Value($dmap[$methName]['docstring']), 'string');
1030             }
1031             else
1032             {
1033                 $r=new Response(new Value('', 'string'));
1034             }
1035         }
1036         else
1037         {
1038             $r=new Response(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']);
1039         }
1040         return $r;
1041     }
1042
1043     public static function _xmlrpcs_multicall_error($err)
1044     {
1045         if(is_string($err))
1046         {
1047             $str = PhpXmlRpc::$xmlrpcstr["multicall_${err}"];
1048             $code = PhpXmlRpc::$xmlrpcerr["multicall_${err}"];
1049         }
1050         else
1051         {
1052             $code = $err->faultCode();
1053             $str = $err->faultString();
1054         }
1055         $struct = array();
1056         $struct['faultCode'] = new Value($code, 'int');
1057         $struct['faultString'] = new Value($str, 'string');
1058         return new Value($struct, 'struct');
1059     }
1060
1061     public static function _xmlrpcs_multicall_do_call($server, $call)
1062     {
1063         if($call->kindOf() != 'struct')
1064         {
1065             return _xmlrpcs_multicall_error('notstruct');
1066         }
1067         $methName = @$call->structmem('methodName');
1068         if(!$methName)
1069         {
1070             return _xmlrpcs_multicall_error('nomethod');
1071         }
1072         if($methName->kindOf() != 'scalar' || $methName->scalartyp() != 'string')
1073         {
1074             return _xmlrpcs_multicall_error('notstring');
1075         }
1076         if($methName->scalarval() == 'system.multicall')
1077         {
1078             return _xmlrpcs_multicall_error('recursion');
1079         }
1080
1081         $params = @$call->structmem('params');
1082         if(!$params)
1083         {
1084             return _xmlrpcs_multicall_error('noparams');
1085         }
1086         if($params->kindOf() != 'array')
1087         {
1088             return _xmlrpcs_multicall_error('notarray');
1089         }
1090         $numParams = $params->arraysize();
1091
1092         $msg = new Request($methName->scalarval());
1093         for($i = 0; $i < $numParams; $i++)
1094         {
1095             if(!$msg->addParam($params->arraymem($i)))
1096             {
1097                 $i++;
1098                 return _xmlrpcs_multicall_error(new Response(0,
1099                     PhpXmlRpc::$xmlrpcerr['incorrect_params'],
1100                     PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": probable xml error in param " . $i));
1101             }
1102         }
1103
1104         $result = $server->execute($msg);
1105
1106         if($result->faultCode() != 0)
1107         {
1108             return _xmlrpcs_multicall_error($result); // Method returned fault.
1109         }
1110
1111         return new Value(array($result->value()), 'array');
1112     }
1113
1114     public static function _xmlrpcs_multicall_do_call_phpvals($server, $call)
1115     {
1116         if(!is_array($call))
1117         {
1118             return _xmlrpcs_multicall_error('notstruct');
1119         }
1120         if(!array_key_exists('methodName', $call))
1121         {
1122             return _xmlrpcs_multicall_error('nomethod');
1123         }
1124         if (!is_string($call['methodName']))
1125         {
1126             return _xmlrpcs_multicall_error('notstring');
1127         }
1128         if($call['methodName'] == 'system.multicall')
1129         {
1130             return _xmlrpcs_multicall_error('recursion');
1131         }
1132         if(!array_key_exists('params', $call))
1133         {
1134             return _xmlrpcs_multicall_error('noparams');
1135         }
1136         if(!is_array($call['params']))
1137         {
1138             return _xmlrpcs_multicall_error('notarray');
1139         }
1140
1141         // this is a real dirty and simplistic hack, since we might have received a
1142         // base64 or datetime values, but they will be listed as strings here...
1143         $numParams = count($call['params']);
1144         $pt = array();
1145         foreach($call['params'] as $val)
1146             $pt[] = php_2_xmlrpc_type(gettype($val));
1147
1148         $result = $server->execute($call['methodName'], $call['params'], $pt);
1149
1150         if($result->faultCode() != 0)
1151         {
1152             return _xmlrpcs_multicall_error($result); // Method returned fault.
1153         }
1154
1155         return new Value(array($result->value()), 'array');
1156     }
1157
1158     public static function _xmlrpcs_multicall($server, $m)
1159     {
1160         $result = array();
1161         // let accept a plain list of php parameters, beside a single xmlrpc msg object
1162         if (is_object($m))
1163         {
1164             $calls = $m->getParam(0);
1165             $numCalls = $calls->arraysize();
1166             for($i = 0; $i < $numCalls; $i++)
1167             {
1168                 $call = $calls->arraymem($i);
1169                 $result[$i] = _xmlrpcs_multicall_do_call($server, $call);
1170             }
1171         }
1172         else
1173         {
1174             $numCalls=count($m);
1175             for($i = 0; $i < $numCalls; $i++)
1176             {
1177                 $result[$i] = _xmlrpcs_multicall_do_call_phpvals($server, $m[$i]);
1178             }
1179         }
1180
1181         return new Response(new Value($result, 'array'));
1182     }
1183 }