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