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