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  
   8  /**
   9   * This class enables the creation of values for XML-RPC, by encapsulating plain php values.
  10   */
  11  class Value implements \Countable, \IteratorAggregate, \ArrayAccess
  12  {
  13      public static $xmlrpcI4 = "i4";
  14      public static $xmlrpcI8 = "i8";
  15      public static $xmlrpcInt = "int";
  16      public static $xmlrpcBoolean = "boolean";
  17      public static $xmlrpcDouble = "double";
  18      public static $xmlrpcString = "string";
  19      public static $xmlrpcDateTime = "dateTime.iso8601";
  20      public static $xmlrpcBase64 = "base64";
  21      public static $xmlrpcArray = "array";
  22      public static $xmlrpcStruct = "struct";
  23      public static $xmlrpcValue = "undefined";
  24      public static $xmlrpcNull = "null";
  25  
  26      public static $xmlrpcTypes = array(
  27          "i4" => 1,
  28          "i8" => 1,
  29          "int" => 1,
  30          "boolean" => 1,
  31          "double" => 1,
  32          "string" => 1,
  33          "dateTime.iso8601" => 1,
  34          "base64" => 1,
  35          "array" => 2,
  36          "struct" => 3,
  37          "null" => 1,
  38      );
  39  
  40      protected static $logger;
  41      protected static $charsetEncoder;
  42  
  43      /// @todo: do these need to be public?
  44      /** @var Value[]|mixed */
  45      public $me = array();
  46      /**
  47       * @var int $mytype
  48       * @internal
  49       */
  50      public $mytype = 0;
  51      /** @var string|null $_php_class */
  52      public $_php_class = null;
  53  
  54      public function getLogger()
  55      {
  56          if (self::$logger === null) {
  57              self::$logger = Logger::instance();
  58          }
  59          return self::$logger;
  60      }
  61  
  62      public static function setLogger($logger)
  63      {
  64          self::$logger = $logger;
  65      }
  66  
  67      public function getCharsetEncoder()
  68      {
  69          if (self::$charsetEncoder === null) {
  70              self::$charsetEncoder = Charset::instance();
  71          }
  72          return self::$charsetEncoder;
  73      }
  74  
  75      public function setCharsetEncoder($charsetEncoder)
  76      {
  77          self::$charsetEncoder = $charsetEncoder;
  78      }
  79  
  80      /**
  81       * Build an xmlrpc value.
  82       *
  83       * When no value or type is passed in, the value is left uninitialized, and the value can be added later.
  84       *
  85       * @param Value[]|mixed $val if passing in an array, all array elements should be PhpXmlRpc\Value themselves
  86       * @param string $type any valid xmlrpc type name (lowercase): i4, int, boolean, string, double, dateTime.iso8601,
  87       *                     base64, array, struct, null.
  88       *                     If null, 'string' is assumed.
  89       *                     You should refer to http://www.xmlrpc.com/spec for more information on what each of these mean.
  90       */
  91      public function __construct($val = -1, $type = '')
  92      {
  93          // optimization creep - do not call addXX, do it all inline.
  94          // downside: booleans will not be coerced anymore
  95          if ($val !== -1 || $type != '') {
  96              switch ($type) {
  97                  case '':
  98                      $this->mytype = 1;
  99                      $this->me['string'] = $val;
 100                      break;
 101                  case 'i4':
 102                  case 'i8':
 103                  case 'int':
 104                  case 'double':
 105                  case 'string':
 106                  case 'boolean':
 107                  case 'dateTime.iso8601':
 108                  case 'base64':
 109                  case 'null':
 110                      $this->mytype = 1;
 111                      $this->me[$type] = $val;
 112                      break;
 113                  case 'array':
 114                      $this->mytype = 2;
 115                      $this->me['array'] = $val;
 116                      break;
 117                  case 'struct':
 118                      $this->mytype = 3;
 119                      $this->me['struct'] = $val;
 120                      break;
 121                  default:
 122                      $this->getLogger()->errorLog("XML-RPC: " . __METHOD__ . ": not a known type ($type)");
 123              }
 124          }
 125      }
 126  
 127      /**
 128       * Add a single php value to an xmlrpc value.
 129       *
 130       * If the xmlrpc value is an array, the php value is added as its last element.
 131       * If the xmlrpc value is empty (uninitialized), this method makes it a scalar value, and sets that value.
 132       * Fails if the xmlrpc value is not an array and already initialized.
 133       *
 134       * @param mixed $val
 135       * @param string $type allowed values: i4, i8, int, boolean, string, double, dateTime.iso8601, base64, null.
 136       *
 137       * @return int 1 or 0 on failure
 138       */
 139      public function addScalar($val, $type = 'string')
 140      {
 141          $typeOf = null;
 142          if (isset(static::$xmlrpcTypes[$type])) {
 143              $typeOf = static::$xmlrpcTypes[$type];
 144          }
 145  
 146          if ($typeOf !== 1) {
 147              $this->getLogger()->errorLog("XML-RPC: " . __METHOD__ . ": not a scalar type ($type)");
 148              return 0;
 149          }
 150  
 151          // coerce booleans into correct values
 152          // NB: we should either do it for datetimes, integers, i8 and doubles, too,
 153          // or just plain remove this check, implemented on booleans only...
 154          if ($type == static::$xmlrpcBoolean) {
 155              if (strcasecmp($val, 'true') == 0 || $val == 1 || ($val == true && strcasecmp($val, 'false'))) {
 156                  $val = true;
 157              } else {
 158                  $val = false;
 159              }
 160          }
 161  
 162          switch ($this->mytype) {
 163              case 1:
 164                  $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': scalar xmlrpc value can have only one value');
 165                  return 0;
 166              case 3:
 167                  $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': cannot add anonymous scalar to struct xmlrpc value');
 168                  return 0;
 169              case 2:
 170                  // we're adding a scalar value to an array here
 171                  $this->me['array'][] = new Value($val, $type);
 172  
 173                  return 1;
 174              default:
 175                  // a scalar, so set the value and remember we're scalar
 176                  $this->me[$type] = $val;
 177                  $this->mytype = $typeOf;
 178  
 179                  return 1;
 180          }
 181      }
 182  
 183      /**
 184       * Add an array of xmlrpc value objects to an xmlrpc value.
 185       *
 186       * If the xmlrpc value is an array, the elements are appended to the existing ones.
 187       * If the xmlrpc value is empty (uninitialized), this method makes it an array value, and sets that value.
 188       * Fails otherwise.
 189       *
 190       * @param Value[] $values
 191       *
 192       * @return int 1 or 0 on failure
 193       *
 194       * @todo add some checking for $values to be an array of xmlrpc values?
 195       */
 196      public function addArray($values)
 197      {
 198          if ($this->mytype == 0) {
 199              $this->mytype = static::$xmlrpcTypes['array'];
 200              $this->me['array'] = $values;
 201  
 202              return 1;
 203          } elseif ($this->mytype == 2) {
 204              // we're adding to an array here
 205              $this->me['array'] = array_merge($this->me['array'], $values);
 206  
 207              return 1;
 208          } else {
 209              $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': already initialized as a [' . $this->kindOf() . ']');
 210              return 0;
 211          }
 212      }
 213  
 214      /**
 215       * Merges an array of named xmlrpc value objects into an xmlrpc value.
 216       *
 217       * If the xmlrpc value is a struct, the elements are merged with the existing ones (overwriting existing ones).
 218       * If the xmlrpc value is empty (uninitialized), this method makes it a struct value, and sets that value.
 219       * Fails otherwise.
 220       *
 221       * @param Value[] $values
 222       *
 223       * @return int 1 or 0 on failure
 224       *
 225       * @todo add some checking for $values to be an array?
 226       */
 227      public function addStruct($values)
 228      {
 229          if ($this->mytype == 0) {
 230              $this->mytype = static::$xmlrpcTypes['struct'];
 231              $this->me['struct'] = $values;
 232  
 233              return 1;
 234          } elseif ($this->mytype == 3) {
 235              // we're adding to a struct here
 236              $this->me['struct'] = array_merge($this->me['struct'], $values);
 237  
 238              return 1;
 239          } else {
 240              $this->getLogger()->errorLog('XML-RPC: ' . __METHOD__ . ': already initialized as a [' . $this->kindOf() . ']');
 241              return 0;
 242          }
 243      }
 244  
 245      /**
 246       * Returns a string containing either "struct", "array", "scalar" or "undef", describing the base type of the value.
 247       *
 248       * @return string
 249       */
 250      public function kindOf()
 251      {
 252          switch ($this->mytype) {
 253              case 3:
 254                  return 'struct';
 255              case 2:
 256                  return 'array';
 257              case 1:
 258                  return 'scalar';
 259              default:
 260                  return 'undef';
 261          }
 262      }
 263  
 264      /**
 265       * @param string $typ
 266       * @param Value[]|mixed $val
 267       * @param string $charsetEncoding
 268       * @return string
 269       */
 270      protected function serializedata($typ, $val, $charsetEncoding = '')
 271      {
 272          $rs = '';
 273  
 274          if (!isset(static::$xmlrpcTypes[$typ])) {
 275              return $rs;
 276          }
 277  
 278          switch (static::$xmlrpcTypes[$typ]) {
 279              case 1:
 280                  switch ($typ) {
 281                      case static::$xmlrpcBase64:
 282                          $rs .= "<$typ}>" . base64_encode($val) . "</$typ}>";
 283                          break;
 284                      case static::$xmlrpcBoolean:
 285                          $rs .= "<$typ}>" . ($val ? '1' : '0') . "</$typ}>";
 286                          break;
 287                      case static::$xmlrpcString:
 288                          // Do NOT use htmlentities, since it will produce named html entities, which are invalid xml
 289                          $rs .= "<$typ}>" . $this->getCharsetEncoder()->encodeEntities($val, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "</$typ}>";
 290                          break;
 291                      case static::$xmlrpcInt:
 292                      case static::$xmlrpcI4:
 293                      case static::$xmlrpcI8:
 294                          $rs .= "<$typ}>" . (int)$val . "</$typ}>";
 295                          break;
 296                      case static::$xmlrpcDouble:
 297                          // avoid using standard conversion of float to string because it is locale-dependent,
 298                          // and also because the xmlrpc spec forbids exponential notation.
 299                          // sprintf('%F') could be most likely ok but it fails eg. on 2e-14.
 300                          // The code below tries its best at keeping max precision while avoiding exp notation,
 301                          // but there is of course no limit in the number of decimal places to be used...
 302                          $rs .= "<$typ}>" . preg_replace('/\\.?0+$/', '', number_format((double)$val, PhpXmlRpc::$xmlpc_double_precision, '.', '')) . "</$typ}>";
 303                          break;
 304                      case static::$xmlrpcDateTime:
 305                          if (is_string($val)) {
 306                              $rs .= "<$typ}>$val}</$typ}>";
 307                          } elseif (is_a($val, 'DateTime') || is_a($val, 'DateTimeInterface')) {
 308                              $rs .= "<$typ}>" . $val->format('Ymd\TH:i:s') . "</$typ}>";
 309                          } elseif (is_int($val)) {
 310                              $rs .= "<$typ}>" . date('Ymd\TH:i:s', $val) . "</$typ}>";
 311                          } else {
 312                              // not really a good idea here: but what should we output anyway? left for backward compat...
 313                              $rs .= "<$typ}>$val}</$typ}>";
 314                          }
 315                          break;
 316                      case static::$xmlrpcNull:
 317                          if (PhpXmlRpc::$xmlrpc_null_apache_encoding) {
 318                              $rs .= "<ex:nil/>";
 319                          } else {
 320                              $rs .= "<nil/>";
 321                          }
 322                          break;
 323                      default:
 324                          // no standard type value should arrive here, but provide a possibility
 325                          // for xmlrpc values of unknown type...
 326                          $rs .= "<$typ}>$val}</$typ}>";
 327                  }
 328                  break;
 329              case 3:
 330                  // struct
 331                  if ($this->_php_class) {
 332                      $rs .= '<struct php_class="' . $this->_php_class . "\">\n";
 333                  } else {
 334                      $rs .= "<struct>\n";
 335                  }
 336                  $charsetEncoder = $this->getCharsetEncoder();
 337                  /** @var Value $val2 */
 338                  foreach ($val as $key2 => $val2) {
 339                      $rs .= '<member><name>' . $charsetEncoder->encodeEntities($key2, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "</name>\n";
 340                      //$rs.=$this->serializeval($val2);
 341                      $rs .= $val2->serialize($charsetEncoding);
 342                      $rs .= "</member>\n";
 343                  }
 344                  $rs .= '</struct>';
 345                  break;
 346              case 2:
 347                  // array
 348                  $rs .= "<array>\n<data>\n";
 349                  /** @var Value $element */
 350                  foreach ($val as $element) {
 351                      //$rs.=$this->serializeval($val[$i]);
 352                      $rs .= $element->serialize($charsetEncoding);
 353                  }
 354                  $rs .= "</data>\n</array>";
 355                  break;
 356              default:
 357                  break;
 358          }
 359  
 360          return $rs;
 361      }
 362  
 363      /**
 364       * Returns the xml representation of the value. XML prologue not included.
 365       *
 366       * @param string $charsetEncoding the charset to be used for serialization. if null, US-ASCII is assumed
 367       *
 368       * @return string
 369       */
 370      public function serialize($charsetEncoding = '')
 371      {
 372          $val = reset($this->me);
 373          $typ = key($this->me);
 374  
 375          return '<value>' . $this->serializedata($typ, $val, $charsetEncoding) . "</value>\n";
 376      }
 377  
 378      /**
 379       * Checks whether a struct member with a given name is present.
 380       *
 381       * Works only on xmlrpc values of type struct.
 382       *
 383       * @param string $key the name of the struct member to be looked up
 384       *
 385       * @return boolean
 386       *
 387       * @deprecated use array access, e.g. isset($val[$key])
 388       */
 389      public function structmemexists($key)
 390      {
 391          //trigger_error('Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED);
 392  
 393          return array_key_exists($key, $this->me['struct']);
 394      }
 395  
 396      /**
 397       * Returns the value of a given struct member (an xmlrpc value object in itself).
 398       * Will raise a php warning if struct member of given name does not exist.
 399       *
 400       * @param string $key the name of the struct member to be looked up
 401       *
 402       * @return Value
 403       *
 404       * @deprecated use array access, e.g. $val[$key]
 405       */
 406      public function structmem($key)
 407      {
 408          //trigger_error('Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED);
 409  
 410          return $this->me['struct'][$key];
 411      }
 412  
 413      /**
 414       * Reset internal pointer for xmlrpc values of type struct.
 415       * @deprecated iterate directly over the object using foreach instead
 416       */
 417      public function structreset()
 418      {
 419          //trigger_error('Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED);
 420  
 421          reset($this->me['struct']);
 422      }
 423  
 424      /**
 425       * Return next member element for xmlrpc values of type struct.
 426       *
 427       * @return Value
 428       * @throws \Error starting with php 8.0, this function should not be used, as it will always throw
 429       *
 430       * @deprecated iterate directly over the object using foreach instead
 431       */
 432      public function structeach()
 433      {
 434          //trigger_error('Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED);
 435  
 436          return @each($this->me['struct']);
 437      }
 438  
 439      /**
 440       * Returns the value of a scalar xmlrpc value (base 64 decoding is automatically handled here)
 441       *
 442       * @return mixed
 443       */
 444      public function scalarval()
 445      {
 446          $b = reset($this->me);
 447  
 448          return $b;
 449      }
 450  
 451      /**
 452       * Returns the type of the xmlrpc value.
 453       *
 454       * For integers, 'int' is always returned in place of 'i4'. 'i8' is considered a separate type and returned as such
 455       *
 456       * @return string
 457       */
 458      public function scalartyp()
 459      {
 460          reset($this->me);
 461          $a = key($this->me);
 462          if ($a == static::$xmlrpcI4) {
 463              $a = static::$xmlrpcInt;
 464          }
 465  
 466          return $a;
 467      }
 468  
 469      /**
 470       * Returns the m-th member of an xmlrpc value of array type.
 471       *
 472       * @param integer $key the index of the value to be retrieved (zero based)
 473       *
 474       * @return Value
 475       *
 476       * @deprecated use array access, e.g. $val[$key]
 477       */
 478      public function arraymem($key)
 479      {
 480          //trigger_error('Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED);
 481  
 482          return $this->me['array'][$key];
 483      }
 484  
 485      /**
 486       * Returns the number of members in an xmlrpc value of array type.
 487       *
 488       * @return integer
 489       *
 490       * @deprecated use count() instead
 491       */
 492      public function arraysize()
 493      {
 494          //trigger_error('Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED);
 495  
 496          return count($this->me['array']);
 497      }
 498  
 499      /**
 500       * Returns the number of members in an xmlrpc value of struct type.
 501       *
 502       * @return integer
 503       *
 504       * @deprecated use count() instead
 505       */
 506      public function structsize()
 507      {
 508          //trigger_error('Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED);
 509  
 510          return count($this->me['struct']);
 511      }
 512  
 513      /**
 514       * Returns the number of members in an xmlrpc value:
 515       * - 0 for uninitialized values
 516       * - 1 for scalar values
 517       * - the number of elements for struct and array values
 518       *
 519       * @return integer
 520       */
 521      #[\ReturnTypeWillChange]
 522      public function count()
 523      {
 524          switch ($this->mytype) {
 525              case 3:
 526                  return count($this->me['struct']);
 527              case 2:
 528                  return count($this->me['array']);
 529              case 1:
 530                  return 1;
 531              default:
 532                  return 0;
 533          }
 534      }
 535  
 536      /**
 537       * Implements the IteratorAggregate interface
 538       *
 539       * @return \ArrayIterator
 540       * @internal required to be public to implement an Interface
 541       */
 542      #[\ReturnTypeWillChange]
 543      public function getIterator()
 544      {
 545          switch ($this->mytype) {
 546              case 3:
 547                  return new \ArrayIterator($this->me['struct']);
 548              case 2:
 549                  return new \ArrayIterator($this->me['array']);
 550              case 1:
 551                  return new \ArrayIterator($this->me);
 552              default:
 553                  return new \ArrayIterator();
 554          }
 555      }
 556  
 557      /**
 558       * @internal required to be public to implement an Interface
 559       * @param mixed $offset
 560       * @param mixed $value
 561       * @throws \Exception
 562       */
 563      #[\ReturnTypeWillChange]
 564      public function offsetSet($offset, $value)
 565      {
 566          switch ($this->mytype) {
 567              case 3:
 568                  if (!($value instanceof \PhpXmlRpc\Value)) {
 569                      throw new \Exception('It is only possible to add Value objects to an XML-RPC Struct');
 570                  }
 571                  if (is_null($offset)) {
 572                      // disallow struct members with empty names
 573                      throw new \Exception('It is not possible to add anonymous members to an XML-RPC Struct');
 574                  } else {
 575                      $this->me['struct'][$offset] = $value;
 576                  }
 577                  return;
 578              case 2:
 579                  if (!($value instanceof \PhpXmlRpc\Value)) {
 580                      throw new \Exception('It is only possible to add Value objects to an XML-RPC Array');
 581                  }
 582                  if (is_null($offset)) {
 583                      $this->me['array'][] = $value;
 584                  } else {
 585                      // nb: we are not checking that $offset is above the existing array range...
 586                      $this->me['array'][$offset] = $value;
 587                  }
 588                  return;
 589              case 1:
 590  // todo: handle i4 vs int
 591                  reset($this->me);
 592                  $type = key($this->me);
 593                  if ($type != $offset) {
 594                      throw new \Exception('');
 595                  }
 596                  $this->me[$type] = $value;
 597                  return;
 598              default:
 599                  // it would be nice to allow empty values to be be turned into non-empty ones this way, but we miss info to do so
 600                  throw new \Exception("XML-RPC Value is of type 'undef' and its value can not be set using array index");
 601          }
 602      }
 603  
 604      /**
 605       * @internal required to be public to implement an Interface
 606       * @param mixed $offset
 607       * @return bool
 608       */
 609      #[\ReturnTypeWillChange]
 610      public function offsetExists($offset)
 611      {
 612          switch ($this->mytype) {
 613              case 3:
 614                  return isset($this->me['struct'][$offset]);
 615              case 2:
 616                  return isset($this->me['array'][$offset]);
 617              case 1:
 618  // todo: handle i4 vs int
 619                  return $offset == $this->scalartyp();
 620              default:
 621                  return false;
 622          }
 623      }
 624  
 625      /**
 626       * @internal required to be public to implement an Interface
 627       * @param mixed $offset
 628       * @throws \Exception
 629       */
 630      #[\ReturnTypeWillChange]
 631      public function offsetUnset($offset)
 632      {
 633          switch ($this->mytype) {
 634              case 3:
 635                  unset($this->me['struct'][$offset]);
 636                  return;
 637              case 2:
 638                  unset($this->me['array'][$offset]);
 639                  return;
 640              case 1:
 641                  // can not remove value from a scalar
 642                  throw new \Exception("XML-RPC Value is of type 'scalar' and its value can not be unset using array index");
 643              default:
 644                  throw new \Exception("XML-RPC Value is of type 'undef' and its value can not be unset using array index");
 645          }
 646      }
 647  
 648      /**
 649       * @internal required to be public to implement an Interface
 650       * @param mixed $offset
 651       * @return mixed|Value|null
 652       * @throws \Exception
 653       */
 654      #[\ReturnTypeWillChange]
 655      public function offsetGet($offset)
 656      {
 657          switch ($this->mytype) {
 658              case 3:
 659                  return isset($this->me['struct'][$offset]) ? $this->me['struct'][$offset] : null;
 660              case 2:
 661                  return isset($this->me['array'][$offset]) ? $this->me['array'][$offset] : null;
 662              case 1:
 663  // on bad type: null or exception?
 664                  $value = reset($this->me);
 665                  $type = key($this->me);
 666                  return $type == $offset ? $value : null;
 667              default:
 668  // return null or exception?
 669                  throw new \Exception("XML-RPC Value is of type 'undef' and can not be accessed using array index");
 670          }
 671      }
 672  }