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\Exception\HttpException;
   6  use PhpXmlRpc\Helper\Charset;
   7  use PhpXmlRpc\Helper\Http;
   8  use PhpXmlRpc\Helper\Logger;
   9  use PhpXmlRpc\Helper\XMLParser;
  10  
  11  /**
  12   * This class provides the representation of a request to an XML-RPC server.
  13   * A client sends a PhpXmlrpc\Request to a server, and receives back an PhpXmlrpc\Response.
  14   */
  15  class Request
  16  {
  17      protected static $logger;
  18      protected static $parser;
  19      protected static $charsetEncoder;
  20  
  21      /// @todo: do these need to be public?
  22      public $payload;
  23      /** @internal */
  24      public $methodname;
  25      /** @internal */
  26      public $params = array();
  27      public $debug = 0;
  28      public $content_type = 'text/xml';
  29  
  30      // holds data while parsing the response. NB: Not a full Response object
  31      /** @deprecated will be removed in a future release */
  32      protected $httpResponse = array();
  33  
  34      public function getLogger()
  35      {
  36          if (self::$logger === null) {
  37              self::$logger = Logger::instance();
  38          }
  39          return self::$logger;
  40      }
  41  
  42      public static function setLogger($logger)
  43      {
  44          self::$logger = $logger;
  45      }
  46  
  47      public function getParser()
  48      {
  49          if (self::$parser === null) {
  50              self::$parser = new XMLParser();
  51          }
  52          return self::$parser;
  53      }
  54  
  55      public static function setParser($parser)
  56      {
  57          self::$parser = $parser;
  58      }
  59  
  60      public function getCharsetEncoder()
  61      {
  62          if (self::$charsetEncoder === null) {
  63              self::$charsetEncoder = Charset::instance();
  64          }
  65          return self::$charsetEncoder;
  66      }
  67  
  68      public function setCharsetEncoder($charsetEncoder)
  69      {
  70          self::$charsetEncoder = $charsetEncoder;
  71      }
  72  
  73      /**
  74       * @param string $methodName the name of the method to invoke
  75       * @param Value[] $params array of parameters to be passed to the method (NB: Value objects, not plain php values)
  76       */
  77      public function __construct($methodName, $params = array())
  78      {
  79          $this->methodname = $methodName;
  80          foreach ($params as $param) {
  81              $this->addParam($param);
  82          }
  83      }
  84  
  85      /**
  86       * @internal this function will become protected in the future
  87       * @param string $charsetEncoding
  88       * @return string
  89       */
  90      public function xml_header($charsetEncoding = '')
  91      {
  92          if ($charsetEncoding != '') {
  93              return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\" ?" . ">\n<methodCall>\n";
  94          } else {
  95              return "<?xml version=\"1.0\"?" . ">\n<methodCall>\n";
  96          }
  97      }
  98  
  99      /**
 100       * @internal this function will become protected in the future
 101       * @return string
 102       */
 103      public function xml_footer()
 104      {
 105          return '</methodCall>';
 106      }
 107  
 108      /**
 109       * @internal this function will become protected in the future
 110       * @param string $charsetEncoding
 111       */
 112      public function createPayload($charsetEncoding = '')
 113      {
 114          if ($charsetEncoding != '') {
 115              $this->content_type = 'text/xml; charset=' . $charsetEncoding;
 116          } else {
 117              $this->content_type = 'text/xml';
 118          }
 119          $this->payload = $this->xml_header($charsetEncoding);
 120          $this->payload .= '<methodName>' . $this->getCharsetEncoder()->encodeEntities(
 121              $this->methodname, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "</methodName>\n";
 122          $this->payload .= "<params>\n";
 123          foreach ($this->params as $p) {
 124              $this->payload .= "<param>\n" . $p->serialize($charsetEncoding) .
 125                  "</param>\n";
 126          }
 127          $this->payload .= "</params>\n";
 128          $this->payload .= $this->xml_footer();
 129      }
 130  
 131      /**
 132       * Gets/sets the xmlrpc method to be invoked.
 133       *
 134       * @param string $methodName the method to be set (leave empty not to set it)
 135       *
 136       * @return string the method that will be invoked
 137       */
 138      public function method($methodName = '')
 139      {
 140          if ($methodName != '') {
 141              $this->methodname = $methodName;
 142          }
 143  
 144          return $this->methodname;
 145      }
 146  
 147      /**
 148       * Returns xml representation of the message. XML prologue included.
 149       *
 150       * @param string $charsetEncoding
 151       *
 152       * @return string the xml representation of the message, xml prologue included
 153       */
 154      public function serialize($charsetEncoding = '')
 155      {
 156          $this->createPayload($charsetEncoding);
 157  
 158          return $this->payload;
 159      }
 160  
 161      /**
 162       * Add a parameter to the list of parameters to be used upon method invocation.
 163       *
 164       * Checks that $params is actually a Value object and not a plain php value.
 165       *
 166       * @param Value $param
 167       *
 168       * @return boolean false on failure
 169       */
 170      public function addParam($param)
 171      {
 172          // check: do not add to self params which are not xmlrpc values
 173          if (is_object($param) && is_a($param, 'PhpXmlRpc\Value')) {
 174              $this->params[] = $param;
 175  
 176              return true;
 177          } else {
 178              return false;
 179          }
 180      }
 181  
 182      /**
 183       * Returns the nth parameter in the request. The index zero-based.
 184       *
 185       * @param integer $i the index of the parameter to fetch (zero based)
 186       *
 187       * @return Value the i-th parameter
 188       */
 189      public function getParam($i)
 190      {
 191          return $this->params[$i];
 192      }
 193  
 194      /**
 195       * Returns the number of parameters in the message.
 196       *
 197       * @return integer the number of parameters currently set
 198       */
 199      public function getNumParams()
 200      {
 201          return count($this->params);
 202      }
 203  
 204      /**
 205       * Given an open file handle, read all data available and parse it as an xmlrpc response.
 206       *
 207       * NB: the file handle is not closed by this function.
 208       * NNB: might have trouble in rare cases to work on network streams, as we check for a read of 0 bytes instead of
 209       *      feof($fp). But since checking for feof(null) returns false, we would risk an infinite loop in that case,
 210       *      because we cannot trust the caller to give us a valid pointer to an open file...
 211       *
 212       * @param resource $fp stream pointer
 213       * @param bool $headersProcessed
 214       * @param string $returnType
 215       *
 216       * @return Response
 217       */
 218      public function parseResponseFile($fp, $headersProcessed = false, $returnType = 'xmlrpcvals')
 219      {
 220          $ipd = '';
 221          while ($data = fread($fp, 32768)) {
 222              $ipd .= $data;
 223          }
 224          return $this->parseResponse($ipd, $headersProcessed, $returnType);
 225      }
 226  
 227      /**
 228       * Parse the xmlrpc response contained in the string $data and return a Response object.
 229       *
 230       * When $this->debug has been set to a value greater than 0, will echo debug messages to screen while decoding.
 231       *
 232       * @param string $data the xmlrpc response, possibly including http headers
 233       * @param bool $headersProcessed when true prevents parsing HTTP headers for interpretation of content-encoding and
 234       *                               consequent decoding
 235       * @param string $returnType decides return type, i.e. content of response->value(). Either 'xmlrpcvals', 'xml' or
 236       *                           'phpvals'
 237       *
 238       * @return Response
 239       *
 240       * @todo parsing Responses is not really the responsibility of the Request class. Maybe of the Client...
 241       */
 242      public function parseResponse($data = '', $headersProcessed = false, $returnType = XMLParser::RETURN_XMLRPCVALS)
 243      {
 244          if ($this->debug) {
 245              $this->getLogger()->debugMessage("---GOT---\n$data\n---END---");
 246          }
 247  
 248          $this->httpResponse = array('raw_data' => $data, 'headers' => array(), 'cookies' => array());
 249  
 250          if ($data == '') {
 251              $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': no response received from server.');
 252              return new Response(0, PhpXmlRpc::$xmlrpcerr['no_data'], PhpXmlRpc::$xmlrpcstr['no_data']);
 253          }
 254  
 255          // parse the HTTP headers of the response, if present, and separate them from data
 256          if (substr($data, 0, 4) == 'HTTP') {
 257              $httpParser = new Http();
 258              try {
 259                  $this->httpResponse = $httpParser->parseResponseHeaders($data, $headersProcessed, $this->debug);
 260              } catch (HttpException $e) {
 261                  // failed processing of HTTP response headers
 262                  // save into response obj the full payload received, for debugging
 263                  return new Response(0, $e->getCode(), $e->getMessage(), '', array('raw_data' => $data, 'status_code', $e->statusCode()));
 264              } catch(\Exception $e) {
 265                  return new Response(0, $e->getCode(), $e->getMessage(), '', array('raw_data' => $data));
 266              }
 267          }
 268  
 269          // be tolerant of extra whitespace in response body
 270          $data = trim($data);
 271  
 272          /// @todo return an error msg if $data == '' ?
 273  
 274          // be tolerant of junk after methodResponse (e.g. javascript ads automatically inserted by free hosts)
 275          // idea from Luca Mariano <luca.mariano@email.it> originally in PEARified version of the lib
 276          $pos = strrpos($data, '</methodResponse>');
 277          if ($pos !== false) {
 278              $data = substr($data, 0, $pos + 17);
 279          }
 280  
 281          // try to 'guestimate' the character encoding of the received response
 282          $respEncoding = XMLParser::guessEncoding(@$this->httpResponse['headers']['content-type'], $data);
 283  
 284          if ($this->debug) {
 285              $start = strpos($data, '<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
 286              if ($start) {
 287                  $start += strlen('<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
 288                  $end = strpos($data, '-->', $start);
 289                  $comments = substr($data, $start, $end - $start);
 290                  $this->getLogger()->debugMessage("---SERVER DEBUG INFO (DECODED) ---\n\t" .
 291                      str_replace("\n", "\n\t", base64_decode($comments)) . "\n---END---", $respEncoding);
 292              }
 293          }
 294  
 295          // if user wants back raw xml, give it to her
 296          if ($returnType == 'xml') {
 297              return new Response($data, 0, '', 'xml', $this->httpResponse);
 298          }
 299  
 300          if ($respEncoding != '') {
 301  
 302              // Since parsing will fail if charset is not specified in the xml prologue,
 303              // the encoding is not UTF8 and there are non-ascii chars in the text, we try to work round that...
 304              // The following code might be better for mb_string enabled installs, but makes the lib about 200% slower...
 305              //if (!is_valid_charset($respEncoding, array('UTF-8')))
 306              if (!in_array($respEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) {
 307                  if ($respEncoding == 'ISO-8859-1') {
 308                      $data = utf8_encode($data);
 309                  } else {
 310  
 311                      if (extension_loaded('mbstring')) {
 312                          $data = mb_convert_encoding($data, 'UTF-8', $respEncoding);
 313                      } else {
 314                          $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': invalid charset encoding of received response: ' . $respEncoding);
 315                      }
 316                  }
 317              }
 318          }
 319  
 320          // PHP internally might use ISO-8859-1, so we have to tell the xml parser to give us back data in the expected charset.
 321          // What if internal encoding is not in one of the 3 allowed? We use the broadest one, ie. utf8
 322          // This allows to send data which is native in various charset, by extending xmlrpc_encode_entities() and
 323          // setting xmlrpc_internalencoding
 324          if (!in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
 325              /// @todo emit a warning
 326              $options = array(XML_OPTION_TARGET_ENCODING => 'UTF-8');
 327          } else {
 328              $options = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
 329          }
 330  
 331          $xmlRpcParser = $this->getParser();
 332          $xmlRpcParser->parse($data, $returnType, XMLParser::ACCEPT_RESPONSE, $options);
 333  
 334          // first error check: xml not well formed
 335          if ($xmlRpcParser->_xh['isf'] > 2) {
 336  
 337              // BC break: in the past for some cases we used the error message: 'XML error at line 1, check URL'
 338  
 339              $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'],
 340                  PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' ' . $xmlRpcParser->_xh['isf_reason'], '',
 341                  $this->httpResponse
 342              );
 343  
 344              if ($this->debug) {
 345                  print $xmlRpcParser->_xh['isf_reason'];
 346              }
 347          }
 348          // second error check: xml well formed but not xml-rpc compliant
 349          elseif ($xmlRpcParser->_xh['isf'] == 2) {
 350              $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'],
 351                  PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' ' . $xmlRpcParser->_xh['isf_reason'], '',
 352                  $this->httpResponse
 353              );
 354  
 355              if ($this->debug) {
 356                  /// @todo echo something for user?
 357              }
 358          }
 359          // third error check: parsing of the response has somehow gone boink.
 360          /// @todo shall we omit this check, since we trust the parsing code?
 361          elseif ($returnType == XMLParser::RETURN_XMLRPCVALS && !is_object($xmlRpcParser->_xh['value'])) {
 362              // something odd has happened
 363              // and it's time to generate a client side error
 364              // indicating something odd went on
 365              $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'], PhpXmlRpc::$xmlrpcstr['invalid_return'],
 366                  '', $this->httpResponse
 367              );
 368          } else {
 369              if ($this->debug > 1) {
 370                  $this->getLogger()->debugMessage(
 371                      "---PARSED---\n".var_export($xmlRpcParser->_xh['value'], true)."\n---END---"
 372                  );
 373              }
 374  
 375              $v = $xmlRpcParser->_xh['value'];
 376  
 377              if ($xmlRpcParser->_xh['isf']) {
 378                  /// @todo we should test here if server sent an int and a string, and/or coerce them into such...
 379                  if ($returnType == XMLParser::RETURN_XMLRPCVALS) {
 380                      $errNo_v = $v['faultCode'];
 381                      $errStr_v = $v['faultString'];
 382                      $errNo = $errNo_v->scalarval();
 383                      $errStr = $errStr_v->scalarval();
 384                  } else {
 385                      $errNo = $v['faultCode'];
 386                      $errStr = $v['faultString'];
 387                  }
 388  
 389                  if ($errNo == 0) {
 390                      // FAULT returned, errno needs to reflect that
 391                      $errNo = -1;
 392                  }
 393  
 394                  $r = new Response(0, $errNo, $errStr, '', $this->httpResponse);
 395              } else {
 396                  $r = new Response($v, 0, '', $returnType, $this->httpResponse);
 397              }
 398          }
 399  
 400          return $r;
 401      }
 402  
 403      /**
 404       * Kept the old name even if Request class was renamed, for compatibility.
 405       *
 406       * @return string
 407       */
 408      public function kindOf()
 409      {
 410          return 'msg';
 411      }
 412  
 413      /**
 414       * Enables/disables the echoing to screen of the xmlrpc responses received.
 415       *
 416       * @param integer $level values 0, 1, 2 are supported
 417       */
 418      public function setDebug($level)
 419      {
 420          $this->debug = $level;
 421      }
 422  }