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