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\Logger;
   6  use PhpXmlRpc\Helper\XMLParser;
   7  
   8  /**
   9   * A helper class to easily convert between Value objects and php native values
  10   * @todo implement an interface
  11   * @todo add class constants for the options values
  12   */
  13  class Encoder
  14  {
  15      protected static $logger;
  16      protected static $parser;
  17  
  18      public function getLogger()
  19      {
  20          if (self::$logger === null) {
  21              self::$logger = Logger::instance();
  22          }
  23          return self::$logger;
  24      }
  25  
  26      public static function setLogger($logger)
  27      {
  28          self::$logger = $logger;
  29      }
  30  
  31      public function getParser()
  32      {
  33          if (self::$parser === null) {
  34              self::$parser = new XMLParser();
  35          }
  36          return self::$parser;
  37      }
  38  
  39      public static function setParser($parser)
  40      {
  41          self::$parser = $parser;
  42      }
  43  
  44      /**
  45       * Takes an xmlrpc value in object format and translates it into native PHP types.
  46       *
  47       * Works with xmlrpc requests objects as input, too.
  48       *
  49       * Given proper options parameter, can rebuild generic php object instances (provided those have been encoded to
  50       * xmlrpc format using a corresponding option in php_xmlrpc_encode())
  51       * PLEASE NOTE that rebuilding php objects involves calling their constructor function.
  52       * This means that the remote communication end can decide which php code will get executed on your server, leaving
  53       * the door possibly open to 'php-injection' style of attacks (provided you have some classes defined on your server
  54       * that might wreak havoc if instances are built outside an appropriate context).
  55       * Make sure you trust the remote server/client before enabling this!
  56       *
  57       * @author Dan Libby (dan@libby.com)
  58       *
  59       * @param Value|Request $xmlrpcVal
  60       * @param array $options if 'decode_php_objs' is set in the options array, xmlrpc structs can be decoded into php
  61       *                       objects; if 'dates_as_objects' is set xmlrpc datetimes are decoded as php DateTime objects
  62       *
  63       * @return mixed
  64       */
  65      public function decode($xmlrpcVal, $options = array())
  66      {
  67          switch ($xmlrpcVal->kindOf()) {
  68              case 'scalar':
  69                  if (in_array('extension_api', $options)) {
  70                      $val = reset($xmlrpcVal->me);
  71                      $typ = key($xmlrpcVal->me);
  72                      switch ($typ) {
  73                          case 'dateTime.iso8601':
  74                              $xmlrpcVal = array(
  75                                  'xmlrpc_type' => 'datetime',
  76                                  'scalar' => $val,
  77                                  'timestamp' => \PhpXmlRpc\Helper\Date::iso8601Decode($val)
  78                              );
  79                              return (object)$xmlrpcVal;
  80                          case 'base64':
  81                              $xmlrpcVal = array(
  82                                  'xmlrpc_type' => 'base64',
  83                                  'scalar' => $val
  84                              );
  85                              return (object)$xmlrpcVal;
  86                          case 'string':
  87                              if (isset($options['extension_api_encoding'])) {
  88                                  $dval = @iconv('UTF-8', $options['extension_api_encoding'], $val);
  89                                  if ($dval !== false) {
  90                                      return $dval;
  91                                  }
  92                              }
  93                              //return $val;
  94                              // break through voluntarily
  95                          default:
  96                              return $val;
  97                      }
  98                  }
  99                  if (in_array('dates_as_objects', $options) && $xmlrpcVal->scalartyp() == 'dateTime.iso8601') {
 100                      // we return a Datetime object instead of a string since now the constructor of xmlrpc value accepts
 101                      // safely strings, ints and datetimes, we cater to all 3 cases here
 102                      $out = $xmlrpcVal->scalarval();
 103                      if (is_string($out)) {
 104                          $out = strtotime($out);
 105                      }
 106                      if (is_int($out)) {
 107                          $result = new \DateTime();
 108                          $result->setTimestamp($out);
 109  
 110                          return $result;
 111                      } elseif (is_a($out, 'DateTimeInterface')) {
 112                          return $out;
 113                      }
 114                  }
 115                  return $xmlrpcVal->scalarval();
 116  
 117              case 'array':
 118                  $arr = array();
 119                  foreach($xmlrpcVal as $value) {
 120                      $arr[] = $this->decode($value, $options);
 121                  }
 122                  return $arr;
 123  
 124              case 'struct':
 125                  // If user said so, try to rebuild php objects for specific struct vals.
 126                  /// @todo should we raise a warning for class not found?
 127                  // shall we check for proper subclass of xmlrpc value instead of presence of _php_class to detect
 128                  // what we can do?
 129                  if (in_array('decode_php_objs', $options) && $xmlrpcVal->_php_class != ''
 130                      && class_exists($xmlrpcVal->_php_class)
 131                  ) {
 132                      $obj = @new $xmlrpcVal->_php_class();
 133                      foreach ($xmlrpcVal as $key => $value) {
 134                          $obj->$key = $this->decode($value, $options);
 135                      }
 136                      return $obj;
 137                  } else {
 138                      $arr = array();
 139                      foreach ($xmlrpcVal as $key => $value) {
 140                          $arr[$key] = $this->decode($value, $options);
 141                      }
 142                      return $arr;
 143                  }
 144  
 145              case 'msg':
 146                  $paramCount = $xmlrpcVal->getNumParams();
 147                  $arr = array();
 148                  for ($i = 0; $i < $paramCount; $i++) {
 149                      $arr[] = $this->decode($xmlrpcVal->getParam($i), $options);
 150                  }
 151                  return $arr;
 152  
 153              /// @todo throw on unsupported type
 154          }
 155      }
 156  
 157      /**
 158       * Takes native php types and encodes them into xmlrpc PHP object format.
 159       * It will not re-encode xmlrpc value objects.
 160       *
 161       * Feature creep -- could support more types via optional type argument
 162       * (string => datetime support has been added, ??? => base64 not yet)
 163       *
 164       * If given a proper options parameter, php object instances will be encoded into 'special' xmlrpc values, that can
 165       * later be decoded into php objects by calling php_xmlrpc_decode() with a corresponding option
 166       *
 167       * @author Dan Libby (dan@libby.com)
 168       *
 169       * @param mixed $phpVal the value to be converted into an xmlrpc value object
 170       * @param array $options can include 'encode_php_objs', 'auto_dates', 'null_extension' or 'extension_api'
 171       *
 172       * @return Value
 173       */
 174      public function encode($phpVal, $options = array())
 175      {
 176          $type = gettype($phpVal);
 177          switch ($type) {
 178              case 'string':
 179                  /// @todo should we be stricter in the accepted dates (ie. reject more of invalid days & times)?
 180                  if (in_array('auto_dates', $options) && preg_match('/^[0-9]{8}T[0-9]{2}:[0-9]{2}:[0-9]{2}$/', $phpVal)) {
 181                      $xmlrpcVal = new Value($phpVal, Value::$xmlrpcDateTime);
 182                  } else {
 183                      $xmlrpcVal = new Value($phpVal, Value::$xmlrpcString);
 184                  }
 185                  break;
 186              case 'integer':
 187                  $xmlrpcVal = new Value($phpVal, Value::$xmlrpcInt);
 188                  break;
 189              case 'double':
 190                  $xmlrpcVal = new Value($phpVal, Value::$xmlrpcDouble);
 191                  break;
 192              // Add support for encoding/decoding of booleans, since they are supported in PHP
 193              case 'boolean':
 194                  $xmlrpcVal = new Value($phpVal, Value::$xmlrpcBoolean);
 195                  break;
 196              case 'array':
 197                  // PHP arrays can be encoded to either xmlrpc structs or arrays, depending on whether they are hashes
 198                  // or plain 0..n integer indexed
 199                  // A shorter one-liner would be
 200                  // $tmp = array_diff(array_keys($phpVal), range(0, count($phpVal)-1));
 201                  // but execution time skyrockets!
 202                  $j = 0;
 203                  $arr = array();
 204                  $ko = false;
 205                  foreach ($phpVal as $key => $val) {
 206                      $arr[$key] = $this->encode($val, $options);
 207                      if (!$ko && $key !== $j) {
 208                          $ko = true;
 209                      }
 210                      $j++;
 211                  }
 212                  if ($ko) {
 213                      $xmlrpcVal = new Value($arr, Value::$xmlrpcStruct);
 214                  } else {
 215                      $xmlrpcVal = new Value($arr, Value::$xmlrpcArray);
 216                  }
 217                  break;
 218              case 'object':
 219                  if (is_a($phpVal, 'PhpXmlRpc\Value')) {
 220                      $xmlrpcVal = $phpVal;
 221                  } elseif (is_a($phpVal, 'DateTimeInterface')) {
 222                      $xmlrpcVal = new Value($phpVal->format('Ymd\TH:i:s'), Value::$xmlrpcDateTime);
 223                  } elseif (in_array('extension_api', $options) && $phpVal instanceof \stdClass && isset($phpVal->xmlrpc_type)) {
 224                      // Handle the 'pre-converted' base64 and datetime values
 225                      if (isset($phpVal->scalar)) {
 226                          switch ($phpVal->xmlrpc_type) {
 227                              case 'base64':
 228                                  $xmlrpcVal = new Value($phpVal->scalar, Value::$xmlrpcBase64);
 229                                  break;
 230                              case 'datetime':
 231                                  $xmlrpcVal = new Value($phpVal->scalar, Value::$xmlrpcDateTime);
 232                                  break;
 233                              default:
 234                                  $xmlrpcVal = new Value();
 235                          }
 236                      } else {
 237                          $xmlrpcVal = new Value();
 238                      }
 239  
 240                  } else {
 241                      $arr = array();
 242                      foreach($phpVal as $k => $v) {
 243                          $arr[$k] = $this->encode($v, $options);
 244                      }
 245                      $xmlrpcVal = new Value($arr, Value::$xmlrpcStruct);
 246                      if (in_array('encode_php_objs', $options)) {
 247                          // let's save original class name into xmlrpc value:
 248                          // might be useful later on...
 249                          $xmlrpcVal->_php_class = get_class($phpVal);
 250                      }
 251                  }
 252                  break;
 253              case 'NULL':
 254                  if (in_array('extension_api', $options)) {
 255                      $xmlrpcVal = new Value('', Value::$xmlrpcString);
 256                  } elseif (in_array('null_extension', $options)) {
 257                      $xmlrpcVal = new Value('', Value::$xmlrpcNull);
 258                  } else {
 259                      $xmlrpcVal = new Value();
 260                  }
 261                  break;
 262              case 'resource':
 263                  if (in_array('extension_api', $options)) {
 264                      $xmlrpcVal = new Value((int)$phpVal, Value::$xmlrpcInt);
 265                  } else {
 266                      $xmlrpcVal = new Value();
 267                  }
 268                  break;
 269              // catch "user function", "unknown type"
 270              default:
 271                  // giancarlo pinerolo <ping@alt.it>
 272                  // it has to return an empty object in case, not a boolean.
 273                  $xmlrpcVal = new Value();
 274                  break;
 275          }
 276  
 277          return $xmlrpcVal;
 278      }
 279  
 280      /**
 281       * Convert the xml representation of a method response, method request or single
 282       * xmlrpc value into the appropriate object (a.k.a. deserialize).
 283       *
 284       * @todo is this a good name/class for this method? It does something quite different from 'decode' after all
 285       *       (returning objects vs returns plain php values)... In fact it belongs rather to a Parser class
 286       *
 287       * @param string $xmlVal
 288       * @param array $options
 289       *
 290       * @return Value|Request|Response|false false on error, or an instance of either Value, Request or Response
 291       */
 292      public function decodeXml($xmlVal, $options = array())
 293      {
 294          // 'guestimate' encoding
 295          $valEncoding = XMLParser::guessEncoding('', $xmlVal);
 296          if ($valEncoding != '') {
 297  
 298              // Since parsing will fail if
 299              // - charset is not specified in the xml prologue,
 300              // - the encoding is not UTF8 and
 301              // - there are non-ascii chars in the text,
 302              // 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($valEncoding, array('UTF-8'))
 305              if (!in_array($valEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($xmlVal)) {
 306                  if ($valEncoding == 'ISO-8859-1') {
 307                      $xmlVal = utf8_encode($xmlVal);
 308                  } else {
 309                      if (extension_loaded('mbstring')) {
 310                          $xmlVal = mb_convert_encoding($xmlVal, 'UTF-8', $valEncoding);
 311                      } else {
 312                          $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': invalid charset encoding of xml text: ' . $valEncoding);
 313                      }
 314                  }
 315              }
 316          }
 317  
 318          // What if internal encoding is not in one of the 3 allowed? We use the broadest one, ie. utf8!
 319          if (!in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) {
 320              /// @todo emit a warning
 321              $parserOptions = array(XML_OPTION_TARGET_ENCODING => 'UTF-8');
 322          } else {
 323              $parserOptions = array(XML_OPTION_TARGET_ENCODING => PhpXmlRpc::$xmlrpc_internalencoding);
 324          }
 325  
 326          $xmlRpcParser = $this->getParser();
 327          $xmlRpcParser->parse(
 328              $xmlVal,
 329              XMLParser::RETURN_XMLRPCVALS,
 330              XMLParser::ACCEPT_REQUEST | XMLParser::ACCEPT_RESPONSE | XMLParser::ACCEPT_VALUE | XMLParser::ACCEPT_FAULT,
 331              $parserOptions
 332          );
 333  
 334          if ($xmlRpcParser->_xh['isf'] > 1) {
 335              // test that $xmlrpc->_xh['value'] is an obj, too???
 336  
 337              $this->getLogger()->errorLog($xmlRpcParser->_xh['isf_reason']);
 338  
 339              return false;
 340          }
 341  
 342          switch ($xmlRpcParser->_xh['rt']) {
 343              case 'methodresponse':
 344                  $v = $xmlRpcParser->_xh['value'];
 345                  if ($xmlRpcParser->_xh['isf'] == 1) {
 346                      /** @var Value $vc */
 347                      $vc = $v['faultCode'];
 348                      /** @var Value $vs */
 349                      $vs = $v['faultString'];
 350                      $r = new Response(0, $vc->scalarval(), $vs->scalarval());
 351                  } else {
 352                      $r = new Response($v);
 353                  }
 354                  return $r;
 355  
 356              case 'methodcall':
 357                  $req = new Request($xmlRpcParser->_xh['method']);
 358                  for ($i = 0; $i < count($xmlRpcParser->_xh['params']); $i++) {
 359                      $req->addParam($xmlRpcParser->_xh['params'][$i]);
 360                  }
 361                  return $req;
 362  
 363              case 'value':
 364                  return $xmlRpcParser->_xh['value'];
 365  
 366              case 'fault':
 367                  // EPI api emulation
 368                  $v = $xmlRpcParser->_xh['value'];
 369                  // use a known error code
 370                  /** @var Value $vc */
 371                  $vc = isset($v['faultCode']) ? $v['faultCode']->scalarval() : PhpXmlRpc::$xmlrpcerr['invalid_return'];
 372                  /** @var Value $vs */
 373                  $vs = isset($v['faultString']) ? $v['faultString']->scalarval() : '';
 374                  if (!is_int($vc) || $vc == 0) {
 375                      $vc = PhpXmlRpc::$xmlrpcerr['invalid_return'];
 376                  }
 377                  return new Response(0, $vc, $vs);
 378              default:
 379                  return false;
 380          }
 381      }
 382  }