Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 401 and 402]

   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  }