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\HttpException;
   6  use PhpXmlRpc\Helper\Http;
   7  use PhpXmlRpc\Helper\XMLParser;
   8  use PhpXmlRpc\Traits\CharsetEncoderAware;
   9  use PhpXmlRpc\Traits\DeprecationLogger;
  10  use PhpXmlRpc\Traits\ParserAware;
  11  use PhpXmlRpc\Traits\PayloadBearer;
  12  
  13  /**
  14   * This class provides the representation of a request to an XML-RPC server.
  15   * A client sends a PhpXmlrpc\Request to a server, and receives back an PhpXmlrpc\Response.
  16   *
  17   * @todo feature creep - add a protected $httpRequest member, in the same way the Response has one
  18   *
  19   * @property string $methodname deprecated - public access left in purely for BC. Access via method()/__construct()
  20   * @property Value[] $params deprecated - public access left in purely for BC. Access via getParam()/__construct()
  21   * @property int $debug deprecated - public access left in purely for BC. Access via .../setDebug()
  22   * @property string $payload deprecated - public access left in purely for BC. Access via getPayload()/setPayload()
  23   * @property string $content_type deprecated - public access left in purely for BC. Access via getContentType()/setPayload()
  24   */
  25  class Request
  26  {
  27      use CharsetEncoderAware;
  28      use DeprecationLogger;
  29      use ParserAware;
  30      use PayloadBearer;
  31  
  32      /** @var string */
  33      protected $methodname;
  34      /** @var Value[] */
  35      protected $params = array();
  36      /** @var int */
  37      protected $debug = 0;
  38  
  39      /**
  40       * holds data while parsing the response. NB: Not a full Response object
  41       * @deprecated will be removed in a future release; still accessible by subclasses for the moment
  42       */
  43      private $httpResponse = array();
  44  
  45      /**
  46       * @param string $methodName the name of the method to invoke
  47       * @param Value[] $params array of parameters to be passed to the method (NB: Value objects, not plain php values)
  48       */
  49      public function __construct($methodName, $params = array())
  50      {
  51          $this->methodname = $methodName;
  52          foreach ($params as $param) {
  53              $this->addParam($param);
  54          }
  55      }
  56  
  57      /**
  58       * Gets/sets the xml-rpc method to be invoked.
  59       *
  60       * @param string $methodName the method to be set (leave empty not to set it)
  61       * @return string the method that will be invoked
  62       */
  63      public function method($methodName = '')
  64      {
  65          if ($methodName != '') {
  66              $this->methodname = $methodName;
  67          }
  68  
  69          return $this->methodname;
  70      }
  71  
  72      /**
  73       * Add a parameter to the list of parameters to be used upon method invocation.
  74       * Checks that $params is actually a Value object and not a plain php value.
  75       *
  76       * @param Value $param
  77       * @return boolean false on failure
  78       */
  79      public function addParam($param)
  80      {
  81          // check: do not add to self params which are not xml-rpc values
  82          if (is_object($param) && is_a($param, 'PhpXmlRpc\Value')) {
  83              $this->params[] = $param;
  84  
  85              return true;
  86          } else {
  87              $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': value passed in must be a PhpXmlRpc\Value');
  88              return false;
  89          }
  90      }
  91  
  92      /**
  93       * Returns the nth parameter in the request. The index zero-based.
  94       *
  95       * @param integer $i the index of the parameter to fetch (zero based)
  96       * @return Value the i-th parameter
  97       */
  98      public function getParam($i)
  99      {
 100          return $this->params[$i];
 101      }
 102  
 103      /**
 104       * Returns the number of parameters in the message.
 105       *
 106       * @return integer the number of parameters currently set
 107       */
 108      public function getNumParams()
 109      {
 110          return count($this->params);
 111      }
 112  
 113      /**
 114       * Returns xml representation of the message, XML prologue included. Sets `payload` and `content_type` properties
 115       *
 116       * @param string $charsetEncoding
 117       * @return string the xml representation of the message, xml prologue included
 118       */
 119      public function serialize($charsetEncoding = '')
 120      {
 121          $this->createPayload($charsetEncoding);
 122  
 123          return $this->payload;
 124      }
 125  
 126      /**
 127       * @internal this function will become protected in the future (and be folded into serialize)
 128       *
 129       * @param string $charsetEncoding
 130       * @return void
 131       */
 132      public function createPayload($charsetEncoding = '')
 133      {
 134          $this->logDeprecationUnlessCalledBy('serialize');
 135  
 136          if ($charsetEncoding != '') {
 137              $this->content_type = 'text/xml; charset=' . $charsetEncoding;
 138          } else {
 139              $this->content_type = 'text/xml';
 140          }
 141  
 142          $result = $this->xml_header($charsetEncoding);
 143          $result .= '<methodName>' . $this->getCharsetEncoder()->encodeEntities(
 144                  $this->methodname, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "</methodName>\n";
 145          $result .= "<params>\n";
 146          foreach ($this->params as $p) {
 147              $result .= "<param>\n" . $p->serialize($charsetEncoding) .
 148                  "</param>\n";
 149          }
 150          $result .= "</params>\n";
 151          $result .= $this->xml_footer();
 152  
 153          $this->payload = $result;
 154      }
 155  
 156      /**
 157       * @internal this function will become protected in the future (and be folded into serialize)
 158       *
 159       * @param string $charsetEncoding
 160       * @return string
 161       */
 162      public function xml_header($charsetEncoding = '')
 163      {
 164          $this->logDeprecationUnlessCalledBy('createPayload');
 165  
 166          if ($charsetEncoding != '') {
 167              return "<?xml version=\"1.0\" encoding=\"$charsetEncoding\" ?" . ">\n<methodCall>\n";
 168          } else {
 169              return "<?xml version=\"1.0\"?" . ">\n<methodCall>\n";
 170          }
 171      }
 172  
 173      /**
 174       * @internal this function will become protected in the future (and be folded into serialize)
 175       *
 176       * @return string
 177       */
 178      public function xml_footer()
 179      {
 180          $this->logDeprecationUnlessCalledBy('createPayload');
 181  
 182          return '</methodCall>';
 183      }
 184  
 185      /**
 186       * Given an open file handle, read all data available and parse it as an xml-rpc response.
 187       *
 188       * NB: the file handle is not closed by this function.
 189       * NNB: might have trouble in rare cases to work on network streams, as we check for a read of 0 bytes instead of
 190       *      feof($fp). But since checking for feof(null) returns false, we would risk an infinite loop in that case,
 191       *      because we cannot trust the caller to give us a valid pointer to an open file...
 192       *
 193       * @param resource $fp stream pointer
 194       * @param bool $headersProcessed
 195       * @param string $returnType
 196       * @return Response
 197       *
 198       * @todo arsing Responses is not really the responsibility of the Request class. Maybe of the Client...
 199       * @todo feature creep - add a flag to disable trying to parse the http headers
 200       */
 201      public function parseResponseFile($fp, $headersProcessed = false, $returnType = 'xmlrpcvals')
 202      {
 203          $ipd = '';
 204          // q: is there an optimal buffer size? Is there any value in making the buffer size a tuneable?
 205          while ($data = fread($fp, 32768)) {
 206              $ipd .= $data;
 207          }
 208          return $this->parseResponse($ipd, $headersProcessed, $returnType);
 209      }
 210  
 211      /**
 212       * Parse the xml-rpc response contained in the string $data and return a Response object.
 213       *
 214       * When $this->debug has been set to a value greater than 0, will echo debug messages to screen while decoding.
 215       *
 216       * @param string $data the xml-rpc response, possibly including http headers
 217       * @param bool $headersProcessed when true prevents parsing HTTP headers for interpretation of content-encoding and
 218       *                               consequent decoding
 219       * @param string $returnType decides return type, i.e. content of response->value(). Either 'xmlrpcvals', 'xml' or
 220       *                           'phpvals'
 221       * @return Response
 222       *
 223       * @todo parsing Responses is not really the responsibility of the Request class. Maybe of the Client...
 224       * @todo what about only populating 'raw_data' in httpResponse when debug mode is > 0?
 225       * @todo feature creep - allow parsing data gotten from a stream pointer instead of a string: read it piecewise,
 226       *       looking first for separation between headers and body, then for charset indicators, server debug info and
 227       *       </methodResponse>. That would require a notable increase in code complexity...
 228       */
 229      public function parseResponse($data = '', $headersProcessed = false, $returnType = XMLParser::RETURN_XMLRPCVALS)
 230      {
 231          if ($this->debug > 0) {
 232              $this->getLogger()->debug("---GOT---\n$data\n---END---");
 233          }
 234  
 235          $this->httpResponse = array('raw_data' => $data, 'headers' => array(), 'cookies' => array());
 236  
 237          if ($data == '') {
 238              $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': no response received from server.');
 239              return new Response(0, PhpXmlRpc::$xmlrpcerr['no_data'], PhpXmlRpc::$xmlrpcstr['no_data']);
 240          }
 241  
 242          // parse the HTTP headers of the response, if present, and separate them from data
 243          if (substr($data, 0, 4) == 'HTTP') {
 244              $httpParser = new Http();
 245              try {
 246                  $httpResponse = $httpParser->parseResponseHeaders($data, $headersProcessed, $this->debug > 0);
 247              } catch (HttpException $e) {
 248                  // failed processing of HTTP response headers
 249                  // save into response obj the full payload received, for debugging
 250                  return new Response(0, $e->getCode(), $e->getMessage(), '', array('raw_data' => $data, 'status_code', $e->statusCode()));
 251              } catch(\Exception $e) {
 252                  return new Response(0, $e->getCode(), $e->getMessage(), '', array('raw_data' => $data));
 253              }
 254          } else {
 255              $httpResponse = $this->httpResponse;
 256          }
 257  
 258          // be tolerant of extra whitespace in response body
 259          $data = trim($data);
 260  
 261          /// @todo optimization creep - return an error msg if $data == ''
 262  
 263          // be tolerant of junk after methodResponse (e.g. javascript ads automatically inserted by free hosts)
 264          // idea from Luca Mariano, originally in PEARified version of the lib
 265          $pos = strrpos($data, '</methodResponse>');
 266          if ($pos !== false) {
 267              $data = substr($data, 0, $pos + 17);
 268          }
 269  
 270          // try to 'guestimate' the character encoding of the received response
 271          $respEncoding = XMLParser::guessEncoding(
 272              isset($httpResponse['headers']['content-type']) ? $httpResponse['headers']['content-type'] : '',
 273              $data
 274          );
 275  
 276          if ($this->debug >= 0) {
 277              $this->httpResponse = $httpResponse;
 278          } else {
 279              $httpResponse = null;
 280          }
 281  
 282          if ($this->debug > 0) {
 283              $start = strpos($data, '<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
 284              if ($start) {
 285                  $start += strlen('<!-- SERVER DEBUG INFO (BASE64 ENCODED):');
 286                  /// @todo what if there is no end tag?
 287                  $end = strpos($data, '-->', $start);
 288                  $comments = substr($data, $start, $end - $start);
 289                  $this->getLogger()->debug("---SERVER DEBUG INFO (DECODED)---\n\t" .
 290                      str_replace("\n", "\n\t", base64_decode($comments)) . "\n---END---", array('encoding' => $respEncoding));
 291              }
 292          }
 293  
 294          // if the user wants back raw xml, give it to her
 295          if ($returnType == 'xml') {
 296              return new Response($data, 0, '', 'xml', $httpResponse);
 297          }
 298  
 299          /// @todo move this block of code into the XMLParser
 300          if ($respEncoding != '') {
 301              // Since parsing will fail if charset is not specified in the xml declaration,
 302              // the encoding is not UTF8 and there are non-ascii chars in the text, we try to work round that...
 303              // The following code might be better for mb_string enabled installs, but makes the lib about 200% slower...
 304              //if (!is_valid_charset($respEncoding, array('UTF-8')))
 305              if (!in_array($respEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) {
 306                  if (function_exists('mb_convert_encoding')) {
 307                      $data = mb_convert_encoding($data, 'UTF-8', $respEncoding);
 308                  } else {
 309                      if ($respEncoding == 'ISO-8859-1') {
 310                          $data = utf8_encode($data);
 311                      } else {
 312                          $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': unsupported charset encoding of received response: ' . $respEncoding);
 313                      }
 314                  }
 315              }
 316          }
 317          // PHP internally might use ISO-8859-1, so we have to tell the xml parser to give us back data in the expected charset.
 318          // What if internal encoding is not in one of the 3 allowed? We use the broadest one, i.e. utf8
 319          if (in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
 320              $options = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
 321          } else {
 322              $options = array(XML_OPTION_TARGET_ENCODING => 'UTF-8', 'target_charset' => PhpXmlRpc::$xmlrpc_internalencoding);
 323          }
 324  
 325          $xmlRpcParser = $this->getParser();
 326          $_xh = $xmlRpcParser->parse($data, $returnType, XMLParser::ACCEPT_RESPONSE, $options);
 327          // BC
 328          if (!is_array($_xh)) {
 329              $_xh = $xmlRpcParser->_xh;
 330          }
 331  
 332          // first error check: xml not well-formed
 333          if ($_xh['isf'] == 3) {
 334  
 335              // BC break: in the past for some cases we used the error message: 'XML error at line 1, check URL'
 336  
 337              // Q: should we give back an error with variable error number, as we do server-side? But if we do, will
 338              //    we be able to tell apart the two cases? In theory, we never emit invalid xml on our end, but
 339              //    there could be proxies meddling with the request, or network data corruption...
 340  
 341              $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_xml'],
 342                  PhpXmlRpc::$xmlrpcstr['invalid_xml'] . ' ' . $_xh['isf_reason'], '', $httpResponse);
 343  
 344              if ($this->debug > 0) {
 345                  $this->getLogger()->debug($_xh['isf_reason']);
 346              }
 347          }
 348          // second error check: xml well-formed but not xml-rpc compliant
 349          elseif ($_xh['isf'] == 2) {
 350              $r = new Response(0, PhpXmlRpc::$xmlrpcerr['xml_not_compliant'],
 351                  PhpXmlRpc::$xmlrpcstr['xml_not_compliant'] . ' ' . $_xh['isf_reason'], '', $httpResponse);
 352  
 353              /// @todo echo something for the user? check if it was already done by the parser...
 354              //if ($this->debug > 0) {
 355              //    $this->getLogger()->debug($_xh['isf_reason']);
 356              //}
 357          }
 358          // third error check: parsing of the response has somehow gone boink.
 359          /// @todo shall we omit this check, since we trust the parsing code?
 360          elseif ($_xh['isf'] > 3 || $returnType == XMLParser::RETURN_XMLRPCVALS && !is_object($_xh['value'])) {
 361              // something odd has happened and it's time to generate a client side error indicating something odd went on
 362              $r = new Response(0, PhpXmlRpc::$xmlrpcerr['xml_parsing_error'], PhpXmlRpc::$xmlrpcstr['xml_parsing_error'],
 363                  '', $httpResponse
 364              );
 365  
 366              /// @todo echo something for the user?
 367          } else {
 368              if ($this->debug > 1) {
 369                  $this->getLogger()->debug(
 370                      "---PARSED---\n".var_export($_xh['value'], true)."\n---END---"
 371                  );
 372              }
 373  
 374              $v = $_xh['value'];
 375  
 376              if ($_xh['isf']) {
 377                  /// @todo we should test (here or preferably in the parser) if server sent an int and a string, and/or
 378                  ///       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                      /// @todo feature creep - add this code to PhpXmlRpc::$xmlrpcerr
 392                      $this->getLogger()->error('XML-RPC: ' . __METHOD__ . ': fault response received with faultCode 0 or null. Converted it to -1');
 393                      /// @todo in Encoder::decodeXML, we use PhpXmlRpc::$xmlrpcerr['invalid_return'] for this case (see
 394                      ///       also the todo 17 lines above)
 395                      $errNo = -1;
 396                  }
 397  
 398                  $r = new Response(0, $errNo, $errStr, '', $httpResponse);
 399              } else {
 400                  $r = new Response($v, 0, '', $returnType, $httpResponse);
 401              }
 402          }
 403  
 404          return $r;
 405      }
 406  
 407      /**
 408       * Kept the old name even if Request class was renamed, for BC.
 409       *
 410       * @return string
 411       */
 412      public function kindOf()
 413      {
 414          return 'msg';
 415      }
 416  
 417      /**
 418       * Enables/disables the echoing to screen of the xml-rpc responses received.
 419       *
 420       * @param integer $level values <0, 0, 1, >1 are supported
 421       * @return $this
 422       */
 423      public function setDebug($level)
 424      {
 425          $this->debug = $level;
 426          return $this;
 427      }
 428  
 429      // *** BC layer ***
 430  
 431      // we have to make this return by ref in order to allow calls such as `$resp->_cookies['name'] = ['value' => 'something'];`
 432      public function &__get($name)
 433      {
 434          switch ($name) {
 435              case 'me':
 436              case 'mytype':
 437              case '_php_class':
 438              case 'payload':
 439              case 'content_type':
 440                  $this->logDeprecation('Getting property Request::' . $name . ' is deprecated');
 441                  return $this->$name;
 442              case 'httpResponse':
 443                  // manually implement the 'protected property' behaviour
 444                  $canAccess = false;
 445                  $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
 446                  if (isset($trace[1]) && isset($trace[1]['class'])) {
 447                      if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Request')) {
 448                          $canAccess = true;
 449                      }
 450                  }
 451                  if ($canAccess) {
 452                      $this->logDeprecation('Getting property Request::' . $name . ' is deprecated');
 453                      return $this->httpResponse;
 454                  } else {
 455                      trigger_error("Cannot access protected property Request::httpResponse in " . __FILE__, E_USER_ERROR);
 456                  }
 457                  break;
 458              default:
 459                  /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
 460                  $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
 461                  trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
 462                  $result = null;
 463                  return $result;
 464          }
 465      }
 466  
 467      public function __set($name, $value)
 468      {
 469          switch ($name) {
 470              case 'methodname':
 471              case 'params':
 472              case 'debug':
 473              case 'payload':
 474              case 'content_type':
 475                  $this->logDeprecation('Setting property Request::' . $name . ' is deprecated');
 476                  $this->$name = $value;
 477                  break;
 478              case 'httpResponse':
 479                  // manually implement the 'protected property' behaviour
 480                  $canAccess = false;
 481                  $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
 482                  if (isset($trace[1]) && isset($trace[1]['class'])) {
 483                      if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Request')) {
 484                          $canAccess = true;
 485                      }
 486                  }
 487                  if ($canAccess) {
 488                      $this->logDeprecation('Setting property Request::' . $name . ' is deprecated');
 489                      $this->httpResponse = $value;
 490                  } else {
 491                      trigger_error("Cannot access protected property Request::httpResponse in " . __FILE__, E_USER_ERROR);
 492                  }
 493                  break;
 494              default:
 495                  /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
 496                  $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
 497                  trigger_error('Undefined property via __set(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
 498          }
 499      }
 500  
 501      public function __isset($name)
 502      {
 503          switch ($name) {
 504              case 'methodname':
 505              case 'params':
 506              case 'debug':
 507              case 'payload':
 508              case 'content_type':
 509                  $this->logDeprecation('Checking property Request::' . $name . ' is deprecated');
 510                  return isset($this->$name);
 511              case 'httpResponse':
 512                  // manually implement the 'protected property' behaviour
 513                  $canAccess = false;
 514                  $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
 515                  if (isset($trace[1]) && isset($trace[1]['class'])) {
 516                      if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Request')) {
 517                          $canAccess = true;
 518                      }
 519                  }
 520                  if ($canAccess) {
 521                      $this->logDeprecation('Checking property Request::' . $name . ' is deprecated');
 522                      return isset($this->httpResponse);
 523                  }
 524                  // break through voluntarily
 525              default:
 526                  return false;
 527          }
 528      }
 529  
 530      public function __unset($name)
 531      {
 532          switch ($name) {
 533              case 'methodname':
 534              case 'params':
 535              case 'debug':
 536              case 'payload':
 537              case 'content_type':
 538                  $this->logDeprecation('Unsetting property Request::' . $name . ' is deprecated');
 539                  unset($this->$name);
 540                  break;
 541              case 'httpResponse':
 542                  // manually implement the 'protected property' behaviour
 543                  $canAccess = false;
 544                  $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
 545                  if (isset($trace[1]) && isset($trace[1]['class'])) {
 546                      if (is_subclass_of($trace[1]['class'], 'PhpXmlRpc\Request')) {
 547                          $canAccess = true;
 548                      }
 549                  }
 550                  if ($canAccess) {
 551                      $this->logDeprecation('Unsetting property Request::' . $name . ' is deprecated');
 552                      unset($this->httpResponse);
 553                  } else {
 554                      trigger_error("Cannot access protected property Request::httpResponse in " . __FILE__, E_USER_ERROR);
 555                  }
 556                  break;
 557              default:
 558                  /// @todo throw instead? There are very few other places where the lib trigger errors which can potentially reach stdout...
 559                  $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
 560                  trigger_error('Undefined property via __unset(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], E_USER_WARNING);
 561          }
 562      }
 563  }