Differences Between: [Versions 401 and 403]
1 <?php 2 3 namespace PhpXmlRpc; 4 5 use PhpXmlRpc\Exception\NoSuchMethodException; 6 use PhpXmlRpc\Exception\ValueErrorException; 7 use PhpXmlRpc\Helper\Http; 8 use PhpXmlRpc\Helper\Interop; 9 use PhpXmlRpc\Helper\Logger; 10 use PhpXmlRpc\Helper\XMLParser; 11 use PhpXmlRpc\Traits\CharsetEncoderAware; 12 use PhpXmlRpc\Traits\DeprecationLogger; 13 use PhpXmlRpc\Traits\ParserAware; 14 15 /** 16 * Allows effortless implementation of XML-RPC servers 17 * 18 * @property string[] $accepted_compression deprecated - public access left in purely for BC. Access via getOption()/setOption() 19 * @property bool $allow_system_funcs deprecated - public access left in purely for BC. Access via getOption()/setOption() 20 * @property bool $compress_response deprecated - public access left in purely for BC. Access via getOption()/setOption() 21 * @property int $debug deprecated - public access left in purely for BC. Access via getOption()/setOption() 22 * @property int $exception_handling deprecated - public access left in purely for BC. Access via getOption()/setOption() 23 * @property string $functions_parameters_type deprecated - public access left in purely for BC. Access via getOption()/setOption() 24 * @property array $phpvals_encoding_options deprecated - public access left in purely for BC. Access via getOption()/setOption() 25 * @property string $response_charset_encoding deprecated - public access left in purely for BC. Access via getOption()/setOption() 26 */ 27 class Server 28 { 29 use CharsetEncoderAware; 30 use DeprecationLogger; 31 use ParserAware; 32 33 const OPT_ACCEPTED_COMPRESSION = 'accepted_compression'; 34 const OPT_ALLOW_SYSTEM_FUNCS = 'allow_system_funcs'; 35 const OPT_COMPRESS_RESPONSE = 'compress_response'; 36 const OPT_DEBUG = 'debug'; 37 const OPT_EXCEPTION_HANDLING = 'exception_handling'; 38 const OPT_FUNCTIONS_PARAMETERS_TYPE = 'functions_parameters_type'; 39 const OPT_PHPVALS_ENCODING_OPTIONS = 'phpvals_encoding_options'; 40 const OPT_RESPONSE_CHARSET_ENCODING = 'response_charset_encoding'; 41 42 /** @var string */ 43 protected static $responseClass = '\\PhpXmlRpc\\Response'; 44 45 /** 46 * @var string 47 * Defines how functions in $dmap will be invoked: either using an xml-rpc Request object or plain php values. 48 * Valid strings are 'xmlrpcvals', 'phpvals' or 'epivals' (only for use by polyfill-xmlrpc). 49 * 50 * @todo create class constants for these 51 */ 52 protected $functions_parameters_type = 'xmlrpcvals'; 53 54 /** 55 * @var array 56 * Option used for fine-tuning the encoding the php values returned from functions registered in the dispatch map 57 * when the functions_parameters_type member is set to 'phpvals'. 58 * @see Encoder::encode for a list of values 59 */ 60 protected $phpvals_encoding_options = array('auto_dates'); 61 62 /** 63 * @var int 64 * Controls whether the server is going to echo debugging messages back to the client as comments in response body. 65 * SECURITY SENSITIVE! 66 * Valid values: 67 * 0 = 68 * 1 = 69 * 2 = 70 * 3 = 71 */ 72 protected $debug = 1; 73 74 /** 75 * @var int 76 * Controls behaviour of server when the invoked method-handler function throws an exception (within the `execute` method): 77 * 0 = catch it and return an 'internal error' xml-rpc response (default) 78 * 1 = SECURITY SENSITIVE DO NOT ENABLE ON PUBLIC SERVERS!!! catch it and return an xml-rpc response with the error 79 * corresponding to the exception, both its code and message. 80 * 2 = allow the exception to float to the upper layers 81 * Can be overridden per-method-handler in the dispatch map 82 */ 83 protected $exception_handling = 0; 84 85 /** 86 * @var bool 87 * When set to true, it will enable HTTP compression of the response, in case the client has declared its support 88 * for compression in the request. 89 * Automatically set at constructor time. 90 */ 91 protected $compress_response = false; 92 93 /** 94 * @var string[] 95 * List of http compression methods accepted by the server for requests. Automatically set at constructor time. 96 * NB: PHP supports deflate, gzip compressions out of the box if compiled w. zlib 97 */ 98 protected $accepted_compression = array(); 99 100 /** 101 * @var bool 102 * Shall we serve calls to system.* methods? 103 */ 104 protected $allow_system_funcs = true; 105 106 /** 107 * List of charset encodings natively accepted for requests. 108 * Set at constructor time. 109 * @deprecated UNUSED so far by this library. It is still accessible by subclasses but will be dropped in the future. 110 */ 111 private $accepted_charset_encodings = array(); 112 113 /** 114 * @var string 115 * Charset encoding to be used for response. 116 * NB: if we can, we will convert the generated response from internal_encoding to the intended one. 117 * Can be: 118 * - a supported xml encoding (only UTF-8 and ISO-8859-1, unless mbstring is enabled), 119 * - null (leave unspecified in response, convert output stream to US_ASCII), 120 * - '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). 121 * NB: pretty dangerous if you accept every charset and do not have mbstring enabled) 122 */ 123 protected $response_charset_encoding = ''; 124 125 protected static $options = array( 126 self::OPT_ACCEPTED_COMPRESSION, 127 self::OPT_ALLOW_SYSTEM_FUNCS, 128 self::OPT_COMPRESS_RESPONSE, 129 self::OPT_DEBUG, 130 self::OPT_EXCEPTION_HANDLING, 131 self::OPT_FUNCTIONS_PARAMETERS_TYPE, 132 self::OPT_PHPVALS_ENCODING_OPTIONS, 133 self::OPT_RESPONSE_CHARSET_ENCODING, 134 ); 135 136 /** 137 * @var mixed 138 * Extra data passed at runtime to method handling functions. Used only by EPI layer 139 * @internal 140 */ 141 public $user_data = null; 142 143 /** 144 * Array defining php functions exposed as xml-rpc methods by this server. 145 * @var array[] $dmap 146 */ 147 protected $dmap = array(); 148 149 /** 150 * Storage for internal debug info. 151 */ 152 protected $debug_info = ''; 153 154 protected static $_xmlrpc_debuginfo = ''; 155 protected static $_xmlrpcs_occurred_errors = ''; 156 protected static $_xmlrpcs_prev_ehandler = ''; 157 158 /** 159 * @param array[] $dispatchMap the dispatch map with definition of exposed services 160 * Array keys are the names of the method names. 161 * Each array value is an array with the following members: 162 * - function (callable) 163 * - docstring (optional) 164 * - signature (array, optional) 165 * - signature_docs (array, optional) 166 * - parameters_type (string, optional) 167 * - exception_handling (int, optional) 168 * @param boolean $serviceNow set to false in order to prevent the server from running upon construction 169 */ 170 public function __construct($dispatchMap = null, $serviceNow = true) 171 { 172 // if ZLIB is enabled, let the server by default accept compressed requests, 173 // and compress responses sent to clients that support them 174 if (function_exists('gzinflate')) { 175 $this->accepted_compression[] = 'gzip'; 176 } 177 if (function_exists('gzuncompress')) { 178 $this->accepted_compression[] = 'deflate'; 179 } 180 if (function_exists('gzencode') || function_exists('gzcompress')) { 181 $this->compress_response = true; 182 } 183 184 // by default the xml parser can support these 3 charset encodings 185 $this->accepted_charset_encodings = array('UTF-8', 'ISO-8859-1', 'US-ASCII'); 186 187 // dispMap is a dispatch array of methods mapped to function names and signatures. 188 // If a method doesn't appear in the map then an unknown method error is generated. 189 // milosch - changed to make passing dispMap optional. Instead, you can use the addToMap() function 190 // to add functions manually (borrowed from SOAPX4) 191 if ($dispatchMap) { 192 $this->setDispatchMap($dispatchMap); 193 if ($serviceNow) { 194 $this->service(); 195 } 196 } 197 } 198 199 /** 200 * @param string $name see all the OPT_ constants 201 * @param mixed $value 202 * @return $this 203 * @throws ValueErrorException on unsupported option 204 */ 205 public function setOption($name, $value) 206 { 207 switch ($name) { 208 case self::OPT_ACCEPTED_COMPRESSION : 209 case self::OPT_ALLOW_SYSTEM_FUNCS: 210 case self::OPT_COMPRESS_RESPONSE: 211 case self::OPT_DEBUG: 212 case self::OPT_EXCEPTION_HANDLING: 213 case self::OPT_FUNCTIONS_PARAMETERS_TYPE: 214 case self::OPT_PHPVALS_ENCODING_OPTIONS: 215 case self::OPT_RESPONSE_CHARSET_ENCODING: 216 $this->$name = $value; 217 break; 218 default: 219 throw new ValueErrorException("Unsupported option '$name'"); 220 } 221 222 return $this; 223 } 224 225 /** 226 * @param string $name see all the OPT_ constants 227 * @return mixed 228 * @throws ValueErrorException on unsupported option 229 */ 230 public function getOption($name) 231 { 232 switch ($name) { 233 case self::OPT_ACCEPTED_COMPRESSION: 234 case self::OPT_ALLOW_SYSTEM_FUNCS: 235 case self::OPT_COMPRESS_RESPONSE: 236 case self::OPT_DEBUG: 237 case self::OPT_EXCEPTION_HANDLING: 238 case self::OPT_FUNCTIONS_PARAMETERS_TYPE: 239 case self::OPT_PHPVALS_ENCODING_OPTIONS: 240 case self::OPT_RESPONSE_CHARSET_ENCODING: 241 return $this->$name; 242 default: 243 throw new ValueErrorException("Unsupported option '$name'"); 244 } 245 } 246 247 /** 248 * Returns the complete list of Server options. 249 * @return array 250 */ 251 public function getOptions() 252 { 253 $values = array(); 254 foreach(static::$options as $opt) { 255 $values[$opt] = $this->getOption($opt); 256 } 257 return $values; 258 } 259 260 /** 261 * @param array $options key: see all the OPT_ constants 262 * @return $this 263 * @throws ValueErrorException on unsupported option 264 */ 265 public function setOptions($options) 266 { 267 foreach($options as $name => $value) { 268 $this->setOption($name, $value); 269 } 270 271 return $this; 272 } 273 274 /** 275 * Set debug level of server. 276 * 277 * @param integer $level debug lvl: determines info added to xml-rpc responses (as xml comments) 278 * 0 = no debug info, 279 * 1 = msgs set from user with debugmsg(), 280 * 2 = add complete xml-rpc request (headers and body), 281 * 3 = add also all processing warnings happened during method processing 282 * (NB: this involves setting a custom error handler, and might interfere 283 * with the standard processing of the php function exposed as method. In 284 * particular, triggering a USER_ERROR level error will not halt script 285 * execution anymore, but just end up logged in the xml-rpc response) 286 * Note that info added at level 2 and 3 will be base64 encoded 287 * @return $this 288 */ 289 public function setDebug($level) 290 { 291 $this->debug = $level; 292 return $this; 293 } 294 295 /** 296 * Add a string to the debug info that can be later serialized by the server as part of the response message. 297 * Note that for best compatibility, the debug string should be encoded using the PhpXmlRpc::$xmlrpc_internalencoding 298 * character set. 299 * 300 * @param string $msg 301 * @return void 302 */ 303 public static function xmlrpc_debugmsg($msg) 304 { 305 static::$_xmlrpc_debuginfo .= $msg . "\n"; 306 } 307 308 /** 309 * Add a string to the debug info that will be later serialized by the server as part of the response message 310 * (base64 encoded) when debug level >= 2 311 * 312 * @param string $msg 313 * @return void 314 */ 315 public static function error_occurred($msg) 316 { 317 static::$_xmlrpcs_occurred_errors .= $msg . "\n"; 318 } 319 320 /** 321 * Return a string with the serialized representation of all debug info. 322 * 323 * @internal this function will become protected in the future 324 * 325 * @param string $charsetEncoding the target charset encoding for the serialization 326 * 327 * @return string an XML comment (or two) 328 */ 329 public function serializeDebug($charsetEncoding = '') 330 { 331 // Tough encoding problem: which internal charset should we assume for debug info? 332 // It might contain a copy of raw data received from client, ie with unknown encoding, 333 // intermixed with php generated data and user generated data... 334 // so we split it: system debug is base 64 encoded, 335 // user debug info should be encoded by the end user using the INTERNAL_ENCODING 336 $out = ''; 337 if ($this->debug_info != '') { 338 $out .= "<!-- SERVER DEBUG INFO (BASE64 ENCODED):\n" . base64_encode($this->debug_info) . "\n-->\n"; 339 } 340 if (static::$_xmlrpc_debuginfo != '') { 341 $out .= "<!-- DEBUG INFO:\n" . $this->getCharsetEncoder()->encodeEntities(str_replace('--', '_-', static::$_xmlrpc_debuginfo), PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "\n-->\n"; 342 // NB: a better solution MIGHT be to use CDATA, but we need to insert it 343 // into return payload AFTER the beginning tag 344 //$out .= "<![CDATA[ DEBUG INFO:\n\n" . str_replace(']]>', ']_]_>', static::$_xmlrpc_debuginfo) . "\n]]>\n"; 345 } 346 347 return $out; 348 } 349 350 /** 351 * Execute the xml-rpc request, printing the response. 352 * 353 * @param string $data the request body. If null, the http POST request will be examined 354 * @param bool $returnPayload When true, return the response but do not echo it or any http header 355 * 356 * @return Response|string the response object (usually not used by caller...) or its xml serialization 357 * @throws \Exception in case the executed method does throw an exception (and depending on server configuration) 358 */ 359 public function service($data = null, $returnPayload = false) 360 { 361 if ($data === null) { 362 $data = file_get_contents('php://input'); 363 } 364 $rawData = $data; 365 366 // reset internal debug info 367 $this->debug_info = ''; 368 369 // Save what we received, before parsing it 370 if ($this->debug > 1) { 371 $this->debugMsg("+++GOT+++\n" . $data . "\n+++END+++"); 372 } 373 374 $resp = $this->parseRequestHeaders($data, $reqCharset, $respCharset, $respEncoding); 375 if (!$resp) { 376 // this actually executes the request 377 $resp = $this->parseRequest($data, $reqCharset); 378 379 // save full body of request into response, for debugging purposes. 380 // NB: this is the _request_ data, not the response's own data, unlike what happens client-side 381 /// @todo try to move this injection to the resp. constructor or use a non-deprecated access method. Or, even 382 /// better: just avoid setting this, and set debug info of the received http request in the request 383 /// object instead? It's not like the developer misses access to _SERVER, _COOKIES though... 384 /// Last but not least: the raw data might be of use to handler functions - but in decompressed form... 385 $resp->raw_data = $rawData; 386 } 387 388 if ($this->debug > 2 && static::$_xmlrpcs_occurred_errors != '') { 389 $this->debugMsg("+++PROCESSING ERRORS AND WARNINGS+++\n" . 390 static::$_xmlrpcs_occurred_errors . "+++END+++"); 391 } 392 393 $header = $resp->xml_header($respCharset); 394 if ($this->debug > 0) { 395 $header .= $this->serializeDebug($respCharset); 396 } 397 398 // Do not create response serialization if it has already happened. Helps to build json magic 399 /// @todo what if the payload was created targeting a different charset than $respCharset? 400 /// Also, if we do not call serialize(), the request will not set its content-type to have the charset declared 401 $payload = $resp->getPayload(); 402 if (empty($payload)) { 403 $payload = $resp->serialize($respCharset); 404 } 405 $payload = $header . $payload; 406 407 if ($returnPayload) { 408 return $payload; 409 } 410 411 // if we get a warning/error that has output some text before here, then we cannot 412 // add a new header. We cannot say we are sending xml, either... 413 if (!headers_sent()) { 414 header('Content-Type: ' . $resp->getContentType()); 415 // we do not know if client actually told us an accepted charset, but if it did we have to tell it what we did 416 header("Vary: Accept-Charset"); 417 418 // http compression of output: only if we can do it, and we want to do it, and client asked us to, 419 // and php ini settings do not force it already 420 $phpNoSelfCompress = !ini_get('zlib.output_compression') && (ini_get('output_handler') != 'ob_gzhandler'); 421 if ($this->compress_response && $respEncoding != '' && $phpNoSelfCompress) { 422 if (strpos($respEncoding, 'gzip') !== false && function_exists('gzencode')) { 423 $payload = gzencode($payload); 424 header("Content-Encoding: gzip"); 425 header("Vary: Accept-Encoding"); 426 } elseif (strpos($respEncoding, 'deflate') !== false && function_exists('gzcompress')) { 427 $payload = gzcompress($payload); 428 header("Content-Encoding: deflate"); 429 header("Vary: Accept-Encoding"); 430 } 431 } 432 433 // Do not output content-length header if php is compressing output for us: it will mess up measurements. 434 // Note that Apache/mod_php will add (and even alter!) the Content-Length header on its own, but only for 435 // responses up to 8000 bytes 436 if ($phpNoSelfCompress) { 437 header('Content-Length: ' . (int)strlen($payload)); 438 } 439 } else { 440 $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': http headers already sent before response is fully generated. Check for php warning or error messages'); 441 } 442 443 print $payload; 444 445 // return response, in case subclasses want it 446 return $resp; 447 } 448 449 /** 450 * Add a method to the dispatch map. 451 * 452 * @param string $methodName the name with which the method will be made available 453 * @param callable $function the php function that will get invoked 454 * @param array[] $sig the array of valid method signatures. 455 * Each element is one signature: an array of strings with at least one element 456 * First element = type of returned value. Elements 2..N = types of parameters 1..N 457 * @param string $doc method documentation 458 * @param array[] $sigDoc the array of valid method signatures docs, following the format of $sig but with 459 * descriptions instead of types (one string for return type, one per param) 460 * @param string $parametersType to allow single method handlers to receive php values instead of a Request, or vice-versa 461 * @param int $exceptionHandling @see $this->exception_handling 462 * @return void 463 * 464 * @todo raise a warning if the user tries to register a 'system.' method 465 */ 466 public function addToMap($methodName, $function, $sig = null, $doc = false, $sigDoc = false, $parametersType = false, 467 $exceptionHandling = false) 468 { 469 $this->add_to_map($methodName, $function, $sig, $doc, $sigDoc, $parametersType, $exceptionHandling); 470 } 471 472 /** 473 * Add a method to the dispatch map. 474 * 475 * @param string $methodName the name with which the method will be made available 476 * @param callable $function the php function that will get invoked 477 * @param array[] $sig the array of valid method signatures. 478 * Each element is one signature: an array of strings with at least one element 479 * First element = type of returned value. Elements 2..N = types of parameters 1..N 480 * @param string $doc method documentation 481 * @param array[] $sigDoc the array of valid method signatures docs, following the format of $sig but with 482 * descriptions instead of types (one string for return type, one per param) 483 * @param string $parametersType to allow single method handlers to receive php values instead of a Request, or vice-versa 484 * @param int $exceptionHandling @see $this->exception_handling 485 * @return void 486 * 487 * @todo raise a warning if the user tries to register a 'system.' method 488 * @deprecated use addToMap instead 489 */ 490 public function add_to_map($methodName, $function, $sig = null, $doc = false, $sigDoc = false, $parametersType = false, 491 $exceptionHandling = false) 492 { 493 $this->logDeprecationUnlessCalledBy('addToMap'); 494 495 $this->dmap[$methodName] = array( 496 'function' => $function, 497 'docstring' => $doc, 498 ); 499 if ($sig) { 500 $this->dmap[$methodName]['signature'] = $sig; 501 } 502 if ($sigDoc) { 503 $this->dmap[$methodName]['signature_docs'] = $sigDoc; 504 } 505 if ($parametersType) { 506 $this->dmap[$methodName]['parameters_type'] = $parametersType; 507 } 508 if ($exceptionHandling !== false) { 509 $this->dmap[$methodName]['exception_handling'] = $exceptionHandling; 510 } 511 } 512 513 /** 514 * Verify type and number of parameters received against a list of known signatures. 515 * 516 * @param array|Request $in array of either xml-rpc value objects or xml-rpc type definitions 517 * @param array $sigs array of known signatures to match against 518 * @return array int, string 519 */ 520 protected function verifySignature($in, $sigs) 521 { 522 // check each possible signature in turn 523 if (is_object($in)) { 524 $numParams = $in->getNumParams(); 525 } else { 526 $numParams = count($in); 527 } 528 foreach ($sigs as $curSig) { 529 if (count($curSig) == $numParams + 1) { 530 $itsOK = 1; 531 for ($n = 0; $n < $numParams; $n++) { 532 if (is_object($in)) { 533 $p = $in->getParam($n); 534 if ($p->kindOf() == 'scalar') { 535 $pt = $p->scalarTyp(); 536 } else { 537 $pt = $p->kindOf(); 538 } 539 } else { 540 $pt = ($in[$n] == 'i4') ? 'int' : strtolower($in[$n]); // dispatch maps never use i4... 541 } 542 543 // param index is $n+1, as first member of sig is return type 544 if ($pt != $curSig[$n + 1] && $curSig[$n + 1] != Value::$xmlrpcValue) { 545 $itsOK = 0; 546 $pno = $n + 1; 547 $wanted = $curSig[$n + 1]; 548 $got = $pt; 549 break; 550 } 551 } 552 if ($itsOK) { 553 return array(1, ''); 554 } 555 } 556 } 557 if (isset($wanted)) { 558 return array(0, "Wanted {$wanted}, got {$got} at param {$pno}"); 559 } else { 560 return array(0, "No method signature matches number of parameters"); 561 } 562 } 563 564 /** 565 * Parse http headers received along with xml-rpc request. If needed, inflate request. 566 * 567 * @return Response|null null on success or an error Response 568 */ 569 protected function parseRequestHeaders(&$data, &$reqEncoding, &$respEncoding, &$respCompression) 570 { 571 // check if $_SERVER is populated: it might have been disabled via ini file 572 // (this is true even when in CLI mode) 573 if (count($_SERVER) == 0) { 574 $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': cannot parse request headers as $_SERVER is not populated'); 575 } 576 577 if ($this->debug > 1) { 578 if (function_exists('getallheaders')) { 579 $this->debugMsg(''); // empty line 580 foreach (getallheaders() as $name => $val) { 581 $this->debugMsg("HEADER: $name: $val"); 582 } 583 } 584 } 585 586 if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) { 587 $contentEncoding = str_replace('x-', '', $_SERVER['HTTP_CONTENT_ENCODING']); 588 } else { 589 $contentEncoding = ''; 590 } 591 592 $rawData = $data; 593 594 // check if request body has been compressed and decompress it 595 if ($contentEncoding != '' && strlen($data)) { 596 if ($contentEncoding == 'deflate' || $contentEncoding == 'gzip') { 597 // if decoding works, use it. else assume data wasn't gzencoded 598 /// @todo test separately for gzinflate and gzuncompress 599 if (function_exists('gzinflate') && in_array($contentEncoding, $this->accepted_compression)) { 600 if ($contentEncoding == 'deflate' && $degzdata = @gzuncompress($data)) { 601 $data = $degzdata; 602 if ($this->debug > 1) { 603 $this->debugMsg("\n+++INFLATED REQUEST+++[" . strlen($data) . " chars]+++\n" . $data . "\n+++END+++"); 604 } 605 } elseif ($contentEncoding == 'gzip' && $degzdata = @gzinflate(substr($data, 10))) { 606 $data = $degzdata; 607 if ($this->debug > 1) { 608 $this->debugMsg("+++INFLATED REQUEST+++[" . strlen($data) . " chars]+++\n" . $data . "\n+++END+++"); 609 } 610 } else { 611 $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_decompress_fail'], 612 PhpXmlRpc::$xmlrpcstr['server_decompress_fail'], '', array('raw_data' => $rawData) 613 ); 614 615 return $r; 616 } 617 } else { 618 $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_cannot_decompress'], 619 PhpXmlRpc::$xmlrpcstr['server_cannot_decompress'], '', array('raw_data' => $rawData) 620 ); 621 622 return $r; 623 } 624 } 625 } 626 627 // check if client specified accepted charsets, and if we know how to fulfill the request 628 if ($this->response_charset_encoding == 'auto') { 629 $respEncoding = ''; 630 if (isset($_SERVER['HTTP_ACCEPT_CHARSET'])) { 631 // here we check if we can match the client-requested encoding with the encodings we know we can generate. 632 // we parse q=0.x preferences instead of preferring the first charset specified 633 $http = new Http(); 634 $clientAcceptedCharsets = $http->parseAcceptHeader($_SERVER['HTTP_ACCEPT_CHARSET']); 635 $knownCharsets = $this->getCharsetEncoder()->knownCharsets(); 636 foreach ($clientAcceptedCharsets as $accepted) { 637 foreach ($knownCharsets as $charset) { 638 if (strtoupper($accepted) == strtoupper($charset)) { 639 $respEncoding = $charset; 640 break 2; 641 } 642 } 643 } 644 } 645 } else { 646 $respEncoding = $this->response_charset_encoding; 647 } 648 649 if (isset($_SERVER['HTTP_ACCEPT_ENCODING'])) { 650 $respCompression = $_SERVER['HTTP_ACCEPT_ENCODING']; 651 } else { 652 $respCompression = ''; 653 } 654 655 // 'guestimate' request encoding 656 /// @todo check if mbstring is enabled and automagic input conversion is on: it might mingle with this check??? 657 $reqEncoding = XMLParser::guessEncoding(isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : '', 658 $data); 659 660 return null; 661 } 662 663 /** 664 * Parse an xml chunk containing an xml-rpc request and execute the corresponding php function registered with the 665 * server. 666 * @internal this function will become protected in the future 667 * 668 * @param string $data the xml request 669 * @param string $reqEncoding (optional) the charset encoding of the xml request 670 * @return Response 671 * @throws \Exception in case the executed method does throw an exception (and depending on server configuration) 672 * 673 * @todo either rename this function or move the 'execute' part out of it... 674 */ 675 public function parseRequest($data, $reqEncoding = '') 676 { 677 // decompose incoming XML into request structure 678 679 /// @todo move this block of code into the XMLParser 680 if ($reqEncoding != '') { 681 // Since parsing will fail if 682 // - charset is not specified in the xml declaration, 683 // - the encoding is not UTF8 and 684 // - there are non-ascii chars in the text, 685 // we try to work round that... 686 // The following code might be better for mb_string enabled installs, but it makes the lib about 200% slower... 687 //if (!is_valid_charset($reqEncoding, array('UTF-8'))) 688 if (!in_array($reqEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) { 689 if (function_exists('mb_convert_encoding')) { 690 $data = mb_convert_encoding($data, 'UTF-8', $reqEncoding); 691 } else { 692 if ($reqEncoding == 'ISO-8859-1') { 693 $data = utf8_encode($data); 694 } else { 695 $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': unsupported charset encoding of received request: ' . $reqEncoding); 696 } 697 } 698 } 699 } 700 // PHP internally might use ISO-8859-1, so we have to tell the xml parser to give us back data in the expected charset. 701 // What if internal encoding is not in one of the 3 allowed? We use the broadest one, i.e. utf8 702 if (in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) { 703 $options = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding); 704 } else { 705 $options = array(XML_OPTION_TARGET_ENCODING => 'UTF-8', 'target_charset' => PhpXmlRpc::$xmlrpc_internalencoding); 706 } 707 // register a callback with the xml parser for when it finds the method name 708 $options['methodname_callback'] = array($this, 'methodNameCallback'); 709 710 $xmlRpcParser = $this->getParser(); 711 try { 712 $_xh = $xmlRpcParser->parse($data, $this->functions_parameters_type, XMLParser::ACCEPT_REQUEST, $options); 713 // BC 714 if (!is_array($_xh)) { 715 $_xh = $xmlRpcParser->_xh; 716 } 717 } catch (NoSuchMethodException $e) { 718 return new static::$responseClass(0, $e->getCode(), $e->getMessage()); 719 } 720 721 if ($_xh['isf'] == 3) { 722 // (BC) we return XML error as a faultCode 723 preg_match('/^XML error ([0-9]+)/', $_xh['isf_reason'], $matches); 724 return new static::$responseClass( 725 0, 726 PhpXmlRpc::$xmlrpcerrxml + (int)$matches[1], 727 $_xh['isf_reason']); 728 } elseif ($_xh['isf']) { 729 /// @todo separate better the various cases, as we have done in Request::parseResponse: invalid xml-rpc vs. 730 /// parsing error 731 return new static::$responseClass( 732 0, 733 PhpXmlRpc::$xmlrpcerr['invalid_request'], 734 PhpXmlRpc::$xmlrpcstr['invalid_request'] . ' ' . $_xh['isf_reason']); 735 } else { 736 // small layering violation in favor of speed and memory usage: we should allow the 'execute' method handle 737 // this, but in the most common scenario (xml-rpc values type server with some methods registered as phpvals) 738 // that would mean a useless encode+decode pass 739 if ($this->functions_parameters_type != 'xmlrpcvals' || 740 (isset($this->dmap[$_xh['method']]['parameters_type']) && 741 ($this->dmap[$_xh['method']]['parameters_type'] != 'xmlrpcvals') 742 ) 743 ) { 744 if ($this->debug > 1) { 745 $this->debugMsg("\n+++PARSED+++\n" . var_export($_xh['params'], true) . "\n+++END+++"); 746 } 747 748 return $this->execute($_xh['method'], $_xh['params'], $_xh['pt']); 749 } else { 750 // build a Request object with data parsed from xml and add parameters in 751 $req = new Request($_xh['method']); 752 /// @todo for more speed, we could just pass in the array to the constructor (and loose the type validation)... 753 for ($i = 0; $i < count($_xh['params']); $i++) { 754 $req->addParam($_xh['params'][$i]); 755 } 756 757 if ($this->debug > 1) { 758 $this->debugMsg("\n+++PARSED+++\n" . var_export($req, true) . "\n+++END+++"); 759 } 760 761 return $this->execute($req); 762 } 763 } 764 } 765 766 /** 767 * Execute a method invoked by the client, checking parameters used. 768 * 769 * @param Request|string $req either a Request obj or a method name 770 * @param mixed[] $params array with method parameters as php types (only if $req is method name) 771 * @param string[] $paramTypes array with xml-rpc types of method parameters (only if $req is method name) 772 * @return Response 773 * 774 * @throws \Exception in case the executed method does throw an exception (and depending on server configuration) 775 */ 776 protected function execute($req, $params = null, $paramTypes = null) 777 { 778 static::$_xmlrpcs_occurred_errors = ''; 779 static::$_xmlrpc_debuginfo = ''; 780 781 if (is_object($req)) { 782 $methodName = $req->method(); 783 } else { 784 $methodName = $req; 785 } 786 787 $sysCall = $this->isSyscall($methodName); 788 $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap; 789 790 if (!isset($dmap[$methodName]['function'])) { 791 // No such method 792 return new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['unknown_method'], PhpXmlRpc::$xmlrpcstr['unknown_method']); 793 } 794 795 // Check signature 796 if (isset($dmap[$methodName]['signature'])) { 797 $sig = $dmap[$methodName]['signature']; 798 if (is_object($req)) { 799 list($ok, $errStr) = $this->verifySignature($req, $sig); 800 } else { 801 list($ok, $errStr) = $this->verifySignature($paramTypes, $sig); 802 } 803 if (!$ok) { 804 // Didn't match. 805 return new static::$responseClass( 806 0, 807 PhpXmlRpc::$xmlrpcerr['incorrect_params'], 808 PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": {$errStr}" 809 ); 810 } 811 } 812 813 $func = $dmap[$methodName]['function']; 814 815 // let the 'class::function' syntax be accepted in dispatch maps 816 if (is_string($func) && strpos($func, '::')) { 817 $func = explode('::', $func); 818 } 819 820 // build string representation of function 'name' 821 if (is_array($func)) { 822 if (is_object($func[0])) { 823 $funcName = get_class($func[0]) . '->' . $func[1]; 824 } else { 825 $funcName = implode('::', $func); 826 } 827 } else if ($func instanceof \Closure) { 828 $funcName = 'Closure'; 829 } else { 830 $funcName = $func; 831 } 832 833 // verify that function to be invoked is in fact callable 834 if (!is_callable($func)) { 835 $this->getLogger()->error("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler is not callable"); 836 return new static::$responseClass( 837 0, 838 PhpXmlRpc::$xmlrpcerr['server_error'], 839 PhpXmlRpc::$xmlrpcstr['server_error'] . ": no function matches method" 840 ); 841 } 842 843 if (isset($dmap[$methodName]['exception_handling'])) { 844 $exception_handling = (int)$dmap[$methodName]['exception_handling']; 845 } else { 846 $exception_handling = $this->exception_handling; 847 } 848 849 // If debug level is 3, we should catch all errors generated during processing of user function, and log them 850 // as part of response 851 if ($this->debug > 2) { 852 self::$_xmlrpcs_prev_ehandler = set_error_handler(array('\PhpXmlRpc\Server', '_xmlrpcs_errorHandler')); 853 } 854 855 try { 856 // Allow mixed-convention servers 857 if (is_object($req)) { 858 // call an 'xml-rpc aware' function 859 if ($sysCall) { 860 $r = call_user_func($func, $this, $req); 861 } else { 862 $r = call_user_func($func, $req); 863 } 864 if (!is_a($r, 'PhpXmlRpc\Response')) { 865 $this->getLogger()->error("XML-RPC: " . __METHOD__ . ": function '$funcName' registered as method handler does not return an xmlrpc response object but a " . gettype($r)); 866 if (is_a($r, 'PhpXmlRpc\Value')) { 867 $r = new static::$responseClass($r); 868 } else { 869 $r = new static::$responseClass( 870 0, 871 PhpXmlRpc::$xmlrpcerr['server_error'], 872 PhpXmlRpc::$xmlrpcstr['server_error'] . ": function does not return xmlrpc response object" 873 ); 874 } 875 } 876 } else { 877 // call a 'plain php' function 878 if ($sysCall) { 879 array_unshift($params, $this); 880 $r = call_user_func_array($func, $params); 881 } else { 882 // 3rd API convention for method-handling functions: EPI-style 883 if ($this->functions_parameters_type == 'epivals') { 884 $r = call_user_func_array($func, array($methodName, $params, $this->user_data)); 885 // mimic EPI behaviour: if we get an array that looks like an error, make it an error response 886 if (is_array($r) && array_key_exists('faultCode', $r) && array_key_exists('faultString', $r)) { 887 $r = new static::$responseClass(0, (integer)$r['faultCode'], (string)$r['faultString']); 888 } else { 889 // functions using EPI api should NOT return resp objects, so make sure we encode the 890 // return type correctly 891 $encoder = new Encoder(); 892 $r = new static::$responseClass($encoder->encode($r, array('extension_api'))); 893 } 894 } else { 895 $r = call_user_func_array($func, $params); 896 } 897 } 898 // the return type can be either a Response object or a plain php value... 899 if (!is_a($r, '\PhpXmlRpc\Response')) { 900 // q: what should we assume here about automatic encoding of datetimes and php classes instances? 901 // a: let the user decide 902 $encoder = new Encoder(); 903 $r = new static::$responseClass($encoder->encode($r, $this->phpvals_encoding_options)); 904 } 905 } 906 /// @todo bump minimum php version to 7.1 and use a single catch clause instead of the duplicate blocks 907 } catch (\Exception $e) { 908 // (barring errors in the lib) an uncaught exception happened in the called function, we wrap it in a 909 // proper error-response 910 switch ($exception_handling) { 911 case 2: 912 if ($this->debug > 2) { 913 if (self::$_xmlrpcs_prev_ehandler) { 914 set_error_handler(self::$_xmlrpcs_prev_ehandler); 915 } else { 916 restore_error_handler(); 917 } 918 } 919 throw $e; 920 case 1: 921 $errCode = $e->getCode(); 922 if ($errCode == 0) { 923 $errCode = PhpXmlRpc::$xmlrpcerr['server_error']; 924 } 925 $r = new static::$responseClass(0, $errCode, $e->getMessage()); 926 break; 927 default: 928 $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']); 929 } 930 } catch (\Error $e) { 931 // (barring errors in the lib) an uncaught exception happened in the called function, we wrap it in a 932 // proper error-response 933 switch ($exception_handling) { 934 case 2: 935 if ($this->debug > 2) { 936 if (self::$_xmlrpcs_prev_ehandler) { 937 set_error_handler(self::$_xmlrpcs_prev_ehandler); 938 } else { 939 restore_error_handler(); 940 } 941 } 942 throw $e; 943 case 1: 944 $errCode = $e->getCode(); 945 if ($errCode == 0) { 946 $errCode = PhpXmlRpc::$xmlrpcerr['server_error']; 947 } 948 $r = new static::$responseClass(0, $errCode, $e->getMessage()); 949 break; 950 default: 951 $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['server_error'], PhpXmlRpc::$xmlrpcstr['server_error']); 952 } 953 } 954 955 if ($this->debug > 2) { 956 // note: restore the error handler we found before calling the user func, even if it has been changed 957 // inside the func itself 958 if (self::$_xmlrpcs_prev_ehandler) { 959 set_error_handler(self::$_xmlrpcs_prev_ehandler); 960 } else { 961 restore_error_handler(); 962 } 963 } 964 965 return $r; 966 } 967 968 /** 969 * Registered as callback for when the XMLParser has found the name of the method to execute. 970 * Handling that early allows to 1. stop parsing the rest of the xml if there is no such method registered, and 971 * 2. tweak the type of data that the parser will return, in case the server uses mixed-calling-convention 972 * 973 * @internal 974 * @param $methodName 975 * @param XMLParser $xmlParser 976 * @param resource $parser 977 * @return void 978 * @throws NoSuchMethodException 979 * 980 * @todo feature creep - we could validate here that the method in the dispatch map is valid, but that would mean 981 * dirtying a lot the logic, as we would have back to both parseRequest() and execute() methods the info 982 * about the matched method handler, in order to avoid doing the work twice... 983 */ 984 public function methodNameCallback($methodName, $xmlParser, $parser) 985 { 986 $sysCall = $this->isSyscall($methodName); 987 $dmap = $sysCall ? $this->getSystemDispatchMap() : $this->dmap; 988 989 if (!isset($dmap[$methodName]['function'])) { 990 // No such method 991 throw new NoSuchMethodException(PhpXmlRpc::$xmlrpcstr['unknown_method'], PhpXmlRpc::$xmlrpcerr['unknown_method']); 992 } 993 994 // alter on-the-fly the config of the xml parser if needed 995 if (isset($dmap[$methodName]['parameters_type']) && 996 $dmap[$methodName]['parameters_type'] != $this->functions_parameters_type) { 997 /// @todo this should be done by a method of the XMLParser 998 switch ($dmap[$methodName]['parameters_type']) { 999 case XMLParser::RETURN_PHP: 1000 xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast'); 1001 break; 1002 case XMLParser::RETURN_EPIVALS: 1003 xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_epi'); 1004 break; 1005 /// @todo log a warning on unsupported return type 1006 case XMLParser::RETURN_XMLRPCVALS: 1007 default: 1008 xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee'); 1009 } 1010 } 1011 } 1012 1013 /** 1014 * Add a string to the 'internal debug message' (separate from 'user debug message'). 1015 * 1016 * @param string $string 1017 * @return void 1018 */ 1019 protected function debugMsg($string) 1020 { 1021 $this->debug_info .= $string . "\n"; 1022 } 1023 1024 /** 1025 * @param string $methName 1026 * @return bool 1027 */ 1028 protected function isSyscall($methName) 1029 { 1030 return (strpos($methName, "system.") === 0); 1031 } 1032 1033 /** 1034 * @param array $dmap 1035 * @return $this 1036 */ 1037 public function setDispatchMap($dmap) 1038 { 1039 $this->dmap = $dmap; 1040 return $this; 1041 } 1042 1043 /** 1044 * @return array[] 1045 */ 1046 public function getDispatchMap() 1047 { 1048 return $this->dmap; 1049 } 1050 1051 /** 1052 * @return array[] 1053 */ 1054 public function getSystemDispatchMap() 1055 { 1056 if (!$this->allow_system_funcs) { 1057 return array(); 1058 } 1059 1060 return array( 1061 'system.listMethods' => array( 1062 'function' => 'PhpXmlRpc\Server::_xmlrpcs_listMethods', 1063 // listMethods: signature was either a string, or nothing. 1064 // The useless string variant has been removed 1065 'signature' => array(array(Value::$xmlrpcArray)), 1066 'docstring' => 'This method lists all the methods that the XML-RPC server knows how to dispatch', 1067 'signature_docs' => array(array('list of method names')), 1068 ), 1069 'system.methodHelp' => array( 1070 'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodHelp', 1071 'signature' => array(array(Value::$xmlrpcString, Value::$xmlrpcString)), 1072 'docstring' => 'Returns help text if defined for the method passed, otherwise returns an empty string', 1073 'signature_docs' => array(array('method description', 'name of the method to be described')), 1074 ), 1075 'system.methodSignature' => array( 1076 'function' => 'PhpXmlRpc\Server::_xmlrpcs_methodSignature', 1077 'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcString)), 1078 '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)', 1079 'signature_docs' => array(array('list of known signatures, each sig being an array of xmlrpc type names', 'name of method to be described')), 1080 ), 1081 'system.multicall' => array( 1082 'function' => 'PhpXmlRpc\Server::_xmlrpcs_multicall', 1083 'signature' => array(array(Value::$xmlrpcArray, Value::$xmlrpcArray)), 1084 'docstring' => 'Boxcar multiple RPC calls in one request. See http://www.xmlrpc.com/discuss/msgReader$1208 for details', 1085 '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"')), 1086 ), 1087 'system.getCapabilities' => array( 1088 'function' => 'PhpXmlRpc\Server::_xmlrpcs_getCapabilities', 1089 'signature' => array(array(Value::$xmlrpcStruct)), 1090 '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', 1091 'signature_docs' => array(array('list of capabilities, described as structs with a version number and url for the spec')), 1092 ), 1093 ); 1094 } 1095 1096 /** 1097 * @return array[] 1098 */ 1099 public function getCapabilities() 1100 { 1101 $outAr = array( 1102 // xml-rpc spec: always supported 1103 'xmlrpc' => array( 1104 'specUrl' => 'http://www.xmlrpc.com/spec', // NB: the spec sits now at http://xmlrpc.com/spec.md 1105 'specVersion' => 1 1106 ), 1107 // if we support system.xxx functions, we always support multicall, too... 1108 'system.multicall' => array( 1109 // Note that, as of 2006/09/17, the following URL does not respond anymore 1110 'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208', 1111 'specVersion' => 1 1112 ), 1113 // introspection: version 2! we support 'mixed', too. 1114 // note: the php xml-rpc extension says this instead: 1115 // url http://xmlrpc-epi.sourceforge.net/specs/rfc.introspection.php, version 20010516 1116 'introspection' => array( 1117 'specUrl' => 'http://phpxmlrpc.sourceforge.net/doc-2/ch10.html', 1118 'specVersion' => 2, 1119 ), 1120 ); 1121 1122 // NIL extension 1123 if (PhpXmlRpc::$xmlrpc_null_extension) { 1124 $outAr['nil'] = array( 1125 // Note that, as of 2023/01, the following URL does not respond anymore 1126 'specUrl' => 'http://www.ontosys.com/xml-rpc/extensions.php', 1127 'specVersion' => 1 1128 ); 1129 } 1130 1131 // support for "standard" error codes 1132 if (PhpXmlRpc::$xmlrpcerr['unknown_method'] === Interop::$xmlrpcerr['unknown_method']) { 1133 $outAr['faults_interop'] = array( 1134 'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php', 1135 'specVersion' => 20010516 1136 ); 1137 } 1138 1139 return $outAr; 1140 } 1141 1142 /** 1143 * @internal handler of a system. method 1144 * 1145 * @param Server $server 1146 * @param Request $req 1147 * @return Response 1148 */ 1149 public static function _xmlrpcs_getCapabilities($server, $req = null) 1150 { 1151 $encoder = new Encoder(); 1152 return new static::$responseClass($encoder->encode($server->getCapabilities())); 1153 } 1154 1155 /** 1156 * @internal handler of a system. method 1157 * 1158 * @param Server $server 1159 * @param Request $req if called in plain php values mode, second param is missing 1160 * @return Response 1161 */ 1162 public static function _xmlrpcs_listMethods($server, $req = null) 1163 { 1164 $outAr = array(); 1165 foreach ($server->dmap as $key => $val) { 1166 $outAr[] = new Value($key, 'string'); 1167 } 1168 foreach ($server->getSystemDispatchMap() as $key => $val) { 1169 $outAr[] = new Value($key, 'string'); 1170 } 1171 1172 return new static::$responseClass(new Value($outAr, 'array')); 1173 } 1174 1175 /** 1176 * @internal handler of a system. method 1177 * 1178 * @param Server $server 1179 * @param Request $req 1180 * @return Response 1181 */ 1182 public static function _xmlrpcs_methodSignature($server, $req) 1183 { 1184 // let's accept as parameter either an xml-rpc value or string 1185 if (is_object($req)) { 1186 $methName = $req->getParam(0); 1187 $methName = $methName->scalarVal(); 1188 } else { 1189 $methName = $req; 1190 } 1191 if ($server->isSyscall($methName)) { 1192 $dmap = $server->getSystemDispatchMap(); 1193 } else { 1194 $dmap = $server->dmap; 1195 } 1196 if (isset($dmap[$methName])) { 1197 if (isset($dmap[$methName]['signature'])) { 1198 $sigs = array(); 1199 foreach ($dmap[$methName]['signature'] as $inSig) { 1200 $curSig = array(); 1201 foreach ($inSig as $sig) { 1202 $curSig[] = new Value($sig, 'string'); 1203 } 1204 $sigs[] = new Value($curSig, 'array'); 1205 } 1206 $r = new static::$responseClass(new Value($sigs, 'array')); 1207 } else { 1208 // NB: according to the official docs, we should be returning a 1209 // "none-array" here, which means not-an-array 1210 $r = new static::$responseClass(new Value('undef', 'string')); 1211 } 1212 } else { 1213 $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']); 1214 } 1215 1216 return $r; 1217 } 1218 1219 /** 1220 * @internal handler of a system. method 1221 * 1222 * @param Server $server 1223 * @param Request $req 1224 * @return Response 1225 */ 1226 public static function _xmlrpcs_methodHelp($server, $req) 1227 { 1228 // let's accept as parameter either an xml-rpc value or string 1229 if (is_object($req)) { 1230 $methName = $req->getParam(0); 1231 $methName = $methName->scalarVal(); 1232 } else { 1233 $methName = $req; 1234 } 1235 if ($server->isSyscall($methName)) { 1236 $dmap = $server->getSystemDispatchMap(); 1237 } else { 1238 $dmap = $server->dmap; 1239 } 1240 if (isset($dmap[$methName])) { 1241 if (isset($dmap[$methName]['docstring'])) { 1242 $r = new static::$responseClass(new Value($dmap[$methName]['docstring'], 'string')); 1243 } else { 1244 $r = new static::$responseClass(new Value('', 'string')); 1245 } 1246 } else { 1247 $r = new static::$responseClass(0, PhpXmlRpc::$xmlrpcerr['introspect_unknown'], PhpXmlRpc::$xmlrpcstr['introspect_unknown']); 1248 } 1249 1250 return $r; 1251 } 1252 1253 /** 1254 * @internal this function will become protected in the future 1255 * 1256 * @param $err 1257 * @return Value 1258 */ 1259 public static function _xmlrpcs_multicall_error($err) 1260 { 1261 if (is_string($err)) { 1262 $str = PhpXmlRpc::$xmlrpcstr["multicall_{$err}"]; 1263 $code = PhpXmlRpc::$xmlrpcerr["multicall_{$err}"]; 1264 } else { 1265 $code = $err->faultCode(); 1266 $str = $err->faultString(); 1267 } 1268 $struct = array(); 1269 $struct['faultCode'] = new Value($code, 'int'); 1270 $struct['faultString'] = new Value($str, 'string'); 1271 1272 return new Value($struct, 'struct'); 1273 } 1274 1275 /** 1276 * @internal this function will become protected in the future 1277 * 1278 * @param Server $server 1279 * @param Value $call 1280 * @return Value 1281 */ 1282 public static function _xmlrpcs_multicall_do_call($server, $call) 1283 { 1284 if ($call->kindOf() != 'struct') { 1285 return static::_xmlrpcs_multicall_error('notstruct'); 1286 } 1287 $methName = @$call['methodName']; 1288 if (!$methName) { 1289 return static::_xmlrpcs_multicall_error('nomethod'); 1290 } 1291 if ($methName->kindOf() != 'scalar' || $methName->scalarTyp() != 'string') { 1292 return static::_xmlrpcs_multicall_error('notstring'); 1293 } 1294 if ($methName->scalarVal() == 'system.multicall') { 1295 return static::_xmlrpcs_multicall_error('recursion'); 1296 } 1297 1298 $params = @$call['params']; 1299 if (!$params) { 1300 return static::_xmlrpcs_multicall_error('noparams'); 1301 } 1302 if ($params->kindOf() != 'array') { 1303 return static::_xmlrpcs_multicall_error('notarray'); 1304 } 1305 1306 $req = new Request($methName->scalarVal()); 1307 foreach ($params as $i => $param) { 1308 if (!$req->addParam($param)) { 1309 $i++; // for error message, we count params from 1 1310 return static::_xmlrpcs_multicall_error(new static::$responseClass(0, 1311 PhpXmlRpc::$xmlrpcerr['incorrect_params'], 1312 PhpXmlRpc::$xmlrpcstr['incorrect_params'] . ": probable xml error in param " . $i)); 1313 } 1314 } 1315 1316 $result = $server->execute($req); 1317 1318 if ($result->faultCode() != 0) { 1319 return static::_xmlrpcs_multicall_error($result); // Method returned fault. 1320 } 1321 1322 return new Value(array($result->value()), 'array'); 1323 } 1324 1325 /** 1326 * @internal this function will become protected in the future 1327 * 1328 * @param Server $server 1329 * @param Value $call 1330 * @return Value 1331 */ 1332 public static function _xmlrpcs_multicall_do_call_phpvals($server, $call) 1333 { 1334 if (!is_array($call)) { 1335 return static::_xmlrpcs_multicall_error('notstruct'); 1336 } 1337 if (!array_key_exists('methodName', $call)) { 1338 return static::_xmlrpcs_multicall_error('nomethod'); 1339 } 1340 if (!is_string($call['methodName'])) { 1341 return static::_xmlrpcs_multicall_error('notstring'); 1342 } 1343 if ($call['methodName'] == 'system.multicall') { 1344 return static::_xmlrpcs_multicall_error('recursion'); 1345 } 1346 if (!array_key_exists('params', $call)) { 1347 return static::_xmlrpcs_multicall_error('noparams'); 1348 } 1349 if (!is_array($call['params'])) { 1350 return static::_xmlrpcs_multicall_error('notarray'); 1351 } 1352 1353 // this is a simplistic hack, since we might have received 1354 // base64 or datetime values, but they will be listed as strings here... 1355 $pt = array(); 1356 $wrapper = new Wrapper(); 1357 foreach ($call['params'] as $val) { 1358 // support EPI-encoded base64 and datetime values 1359 if ($val instanceof \stdClass && isset($val->xmlrpc_type)) { 1360 $pt[] = $val->xmlrpc_type == 'datetime' ? Value::$xmlrpcDateTime : $val->xmlrpc_type; 1361 } else { 1362 $pt[] = $wrapper->php2XmlrpcType(gettype($val)); 1363 } 1364 } 1365 1366 $result = $server->execute($call['methodName'], $call['params'], $pt); 1367 1368 if ($result->faultCode() != 0) { 1369 return static::_xmlrpcs_multicall_error($result); // Method returned fault. 1370 } 1371 1372 return new Value(array($result->value()), 'array'); 1373 } 1374 1375 /** 1376 * @internal handler of a system. method 1377 * 1378 * @param Server $server 1379 * @param Request|array $req 1380 * @return Response 1381 */ 1382 public static function _xmlrpcs_multicall($server, $req) 1383 { 1384 $result = array(); 1385 // let's accept a plain list of php parameters, beside a single xml-rpc msg object 1386 if (is_object($req)) { 1387 $calls = $req->getParam(0); 1388 foreach ($calls as $call) { 1389 $result[] = static::_xmlrpcs_multicall_do_call($server, $call); 1390 } 1391 } else { 1392 $numCalls = count($req); 1393 for ($i = 0; $i < $numCalls; $i++) { 1394 $result[$i] = static::_xmlrpcs_multicall_do_call_phpvals($server, $req[$i]); 1395 } 1396 } 1397 1398 return new static::$responseClass(new Value($result, 'array')); 1399 } 1400 1401 /** 1402 * Error handler used to track errors that occur during server-side execution of PHP code. 1403 * This allows to report back to the client whether an internal error has occurred or not 1404 * using an xml-rpc response object, instead of letting the client deal with the html junk 1405 * that a PHP execution error on the server generally entails. 1406 * 1407 * NB: in fact a user defined error handler can only handle WARNING, NOTICE and USER_* errors. 1408 * 1409 * @internal 1410 */ 1411 public static function _xmlrpcs_errorHandler($errCode, $errString, $filename = null, $lineNo = null, $context = null) 1412 { 1413 // obey the @ protocol 1414 if (error_reporting() == 0) { 1415 return; 1416 } 1417 1418 //if ($errCode != E_NOTICE && $errCode != E_WARNING && $errCode != E_USER_NOTICE && $errCode != E_USER_WARNING) 1419 if ($errCode != E_STRICT) { 1420 static::error_occurred($errString); 1421 } 1422 1423 // Try to avoid as much as possible disruption to the previous error handling mechanism in place 1424 if (self::$_xmlrpcs_prev_ehandler == '') { 1425 // The previous error handler was the default: all we should do is log error to the default error log 1426 // (if level high enough) 1427 if (ini_get('log_errors') && (intval(ini_get('error_reporting')) & $errCode)) { 1428 // we can't use the functionality of LoggerAware, because this is a static method 1429 if (self::$logger === null) { 1430 self::$logger = Logger::instance(); 1431 } 1432 self::$logger->error($errString); 1433 } 1434 } else { 1435 // Pass control on to previous error handler, trying to avoid loops... 1436 if (self::$_xmlrpcs_prev_ehandler != array('\PhpXmlRpc\Server', '_xmlrpcs_errorHandler')) { 1437 if (is_array(self::$_xmlrpcs_prev_ehandler)) { 1438 // the following works both with static class methods and plain object methods as error handler 1439 call_user_func_array(self::$_xmlrpcs_prev_ehandler, array($errCode, $errString, $filename, $lineNo, $context)); 1440 } else { 1441 $method = self::$_xmlrpcs_prev_ehandler; 1442 $method($errCode, $errString, $filename, $lineNo, $context); 1443 } 1444 } 1445 } 1446 } 1447 1448 // *** BC layer *** 1449 1450 /** 1451 * @param string $charsetEncoding 1452 * @return string 1453 * 1454 * @deprecated this method was moved to the Response class 1455 */ 1456 protected function xml_header($charsetEncoding = '') 1457 { 1458 $this->logDeprecation('Method ' . __METHOD__ . ' is deprecated'); 1459 1460 if ($charsetEncoding != '') { 1461 return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\"?" . ">\n"; 1462 } else { 1463 return "<?xml version=\"1.0\"?" . ">\n"; 1464 } 1465 } 1466 1467 // we have to make this return by ref in order to allow calls such as `$resp->_cookies['name'] = ['value' => 'something'];` 1468 public function &__get($name) 1469 { 1470 switch ($name) { 1471 case self::OPT_ACCEPTED_COMPRESSION : 1472 case self::OPT_ALLOW_SYSTEM_FUNCS: 1473 case self::OPT_COMPRESS_RESPONSE: 1474 case self::OPT_DEBUG: 1475 case self::OPT_EXCEPTION_HANDLING: 1476 case self::OPT_FUNCTIONS_PARAMETERS_TYPE: 1477 case self::OPT_PHPVALS_ENCODING_OPTIONS: 1478 case self::OPT_RESPONSE_CHARSET_ENCODING: 1479 $this->logDeprecation('Getting property Request::' . $name . ' is deprecated'); 1480 return $this->$name; 1481 case 'accepted_charset_encodings': 1482 // manually implement the 'protected property' behaviour 1483 $canAccess = false; 1484 $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); 1485 if (isset($trace[1]) && isset($trace[1]['class'])) { 1486 if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) { 1487 $canAccess = true; 1488 } 1489 } 1490 if ($canAccess) { 1491 $this->logDeprecation('Getting property Request::' . $name . ' is deprecated'); 1492 return $this->accepted_compression; 1493 } else { 1494 trigger_error("Cannot access protected property Server::accepted_charset_encodings in " . __FILE__, E_USER_ERROR); 1495 } 1496 break; 1497 default: 1498 /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout... 1499 $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1); 1500 trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING); 1501 $result = null; 1502 return $result; 1503 } 1504 } 1505 1506 public function __set($name, $value) 1507 { 1508 switch ($name) { 1509 case self::OPT_ACCEPTED_COMPRESSION : 1510 case self::OPT_ALLOW_SYSTEM_FUNCS: 1511 case self::OPT_COMPRESS_RESPONSE: 1512 case self::OPT_DEBUG: 1513 case self::OPT_EXCEPTION_HANDLING: 1514 case self::OPT_FUNCTIONS_PARAMETERS_TYPE: 1515 case self::OPT_PHPVALS_ENCODING_OPTIONS: 1516 case self::OPT_RESPONSE_CHARSET_ENCODING: 1517 $this->logDeprecation('Setting property Request::' . $name . ' is deprecated'); 1518 $this->$name = $value; 1519 break; 1520 case 'accepted_charset_encodings': 1521 // manually implement the 'protected property' behaviour 1522 $canAccess = false; 1523 $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); 1524 if (isset($trace[1]) && isset($trace[1]['class'])) { 1525 if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) { 1526 $canAccess = true; 1527 } 1528 } 1529 if ($canAccess) { 1530 $this->logDeprecation('Setting property Request::' . $name . ' is deprecated'); 1531 $this->accepted_compression = $value; 1532 } else { 1533 trigger_error("Cannot access protected property Server::accepted_charset_encodings in " . __FILE__, E_USER_ERROR); 1534 } 1535 break; 1536 default: 1537 /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout... 1538 $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1); 1539 trigger_error('Undefined property via __set(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING); 1540 } 1541 } 1542 1543 public function __isset($name) 1544 { 1545 switch ($name) { 1546 case self::OPT_ACCEPTED_COMPRESSION : 1547 case self::OPT_ALLOW_SYSTEM_FUNCS: 1548 case self::OPT_COMPRESS_RESPONSE: 1549 case self::OPT_DEBUG: 1550 case self::OPT_EXCEPTION_HANDLING: 1551 case self::OPT_FUNCTIONS_PARAMETERS_TYPE: 1552 case self::OPT_PHPVALS_ENCODING_OPTIONS: 1553 case self::OPT_RESPONSE_CHARSET_ENCODING: 1554 $this->logDeprecation('Checking property Request::' . $name . ' is deprecated'); 1555 return isset($this->$name); 1556 case 'accepted_charset_encodings': 1557 // manually implement the 'protected property' behaviour 1558 $canAccess = false; 1559 $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); 1560 if (isset($trace[1]) && isset($trace[1]['class'])) { 1561 if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) { 1562 $canAccess = true; 1563 } 1564 } 1565 if ($canAccess) { 1566 $this->logDeprecation('Checking property Request::' . $name . ' is deprecated'); 1567 return isset($this->accepted_compression); 1568 } 1569 // break through voluntarily 1570 default: 1571 return false; 1572 } 1573 } 1574 1575 public function __unset($name) 1576 { 1577 switch ($name) { 1578 case self::OPT_ACCEPTED_COMPRESSION : 1579 case self::OPT_ALLOW_SYSTEM_FUNCS: 1580 case self::OPT_COMPRESS_RESPONSE: 1581 case self::OPT_DEBUG: 1582 case self::OPT_EXCEPTION_HANDLING: 1583 case self::OPT_FUNCTIONS_PARAMETERS_TYPE: 1584 case self::OPT_PHPVALS_ENCODING_OPTIONS: 1585 case self::OPT_RESPONSE_CHARSET_ENCODING: 1586 $this->logDeprecation('Unsetting property Request::' . $name . ' is deprecated'); 1587 unset($this->$name); 1588 break; 1589 case 'accepted_charset_encodings': 1590 // manually implement the 'protected property' behaviour 1591 $canAccess = false; 1592 $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); 1593 if (isset($trace[1]) && isset($trace[1]['class'])) { 1594 if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Server')) { 1595 $canAccess = true; 1596 } 1597 } 1598 if ($canAccess) { 1599 $this->logDeprecation('Unsetting property Request::' . $name . ' is deprecated'); 1600 unset($this->accepted_compression); 1601 } else { 1602 trigger_error("Cannot access protected property Server::accepted_charset_encodings in " . __FILE__, E_USER_ERROR); 1603 } 1604 break; 1605 default: 1606 /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout... 1607 $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1); 1608 trigger_error('Undefined property via __unset(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING); 1609 } 1610 } 1611 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body