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