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