Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 401 and 402] [Versions 401 and 403]

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