Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]

   1  <?php
   2  /**
   3   * Custom XML parser for signed and/or encrypted XML Docs
   4   *
   5   * @author  Donal McMullan  donal@catalyst.net.nz
   6   * @version 0.0.1
   7   * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
   8   * @package mnet
   9   */
  10  
  11  /**
  12   * Custom XML parser class for signed and/or encrypted XML Docs
  13   */
  14  class mnet_encxml_parser {
  15  
  16      /** @var resource|false|XMLParser — a resource handle for the new XML parser. */
  17      private $parser;
  18  
  19      /** @var int unique ID for each tag. */
  20      private $tag_number;
  21  
  22      /** @var string digest string. */
  23      private $digest;
  24  
  25      /** @var string remote_timestamp string. */
  26      public $remote_timestamp;
  27  
  28      /** @var string remote_wwwroot string. */
  29      public $remote_wwwroot;
  30  
  31      /** @var string signature string. */
  32      public $signature;
  33  
  34      /** @var string data_object string. */
  35      public $data_object;
  36  
  37      /** @var string URI value inside the RETRIEVALMETHOD xml tag. */
  38      private $key_URI;
  39  
  40      /** @var bool true if $chiper has a value, otherwise false. */
  41      public $payload_encrypted;
  42  
  43      /** @var array the chiper string. */
  44      public $cipher = [];
  45  
  46      /** @var array error information with code and string keys. */
  47      public $error = [];
  48  
  49      /** @var string The remote error string, specified in the discard_data(). */
  50      public $remoteerror;
  51  
  52      /** @var stdClass error started status. */
  53      private $errorstarted;
  54  
  55      /**
  56       * Constructor creates and initialises parser resource and calls initialise
  57       *
  58       * @return bool True
  59       */
  60      public function __construct() {
  61          return $this->initialise();
  62      }
  63  
  64      /**
  65       * Old syntax of class constructor. Deprecated in PHP7.
  66       *
  67       * @deprecated since Moodle 3.1
  68       */
  69      public function mnet_encxml_parser() {
  70          debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
  71          self::__construct();
  72      }
  73  
  74      /**
  75       * Set default element handlers and initialise properties to empty.
  76       *
  77       * @return bool True
  78       */
  79      function initialise() {
  80          $this->parser = xml_parser_create();
  81          xml_set_object($this->parser, $this);
  82  
  83          xml_set_element_handler($this->parser, "start_element", "end_element");
  84          xml_set_character_data_handler($this->parser, "discard_data");
  85  
  86          $this->tag_number        = 0; // Just a unique ID for each tag
  87          $this->digest            = '';
  88          $this->remote_timestamp  = '';
  89          $this->remote_wwwroot    = '';
  90          $this->signature         = '';
  91          $this->data_object       = '';
  92          $this->key_URI           = '';
  93          $this->payload_encrypted = false;
  94          $this->cipher            = array();
  95          $this->error             = array();
  96          $this->remoteerror       = null;
  97          $this->errorstarted      = false;
  98          return true;
  99      }
 100  
 101      /**
 102       * Parse a block of XML text
 103       *
 104       * The XML Text will be an XML-RPC request which is wrapped in an XML doc
 105       * with a signature from the sender. This envelope may be encrypted and
 106       * delivered within another XML envelope with a symmetric key. The parser
 107       * should first decrypt this XML, and then place the XML-RPC request into
 108       * the data_object property, and the signature into the signature property.
 109       *
 110       * See the W3C's {@link http://www.w3.org/TR/xmlenc-core/ XML Encryption Syntax and Processing}
 111       * and {@link http://www.w3.org/TR/2001/PR-xmldsig-core-20010820/ XML-Signature Syntax and Processing}
 112       * guidelines for more detail on the XML.
 113       *
 114       * -----XML-Envelope---------------------------------
 115       * |                                                |
 116       * |    Symmetric-key--------------------------     |
 117       * |    |_____________________________________|     |
 118       * |                                                |
 119       * |    Encrypted data-------------------------     |
 120       * |    |                                     |     |
 121       * |    |  -XML-Envelope------------------    |     |
 122       * |    |  |                             |    |     |
 123       * |    |  |  --Signature-------------   |    |     |
 124       * |    |  |  |______________________|   |    |     |
 125       * |    |  |                             |    |     |
 126       * |    |  |  --Signed-Payload--------   |    |     |
 127       * |    |  |  |                      |   |    |     |
 128       * |    |  |  |   XML-RPC Request    |   |    |     |
 129       * |    |  |  |______________________|   |    |     |
 130       * |    |  |                             |    |     |
 131       * |    |  |_____________________________|    |     |
 132       * |    |_____________________________________|     |
 133       * |                                                |
 134       * |________________________________________________|
 135       *
 136       * @param   string  $data   The XML that you want to parse
 137       * @return  bool            True on success - false on failure
 138       */
 139      function parse($data) {
 140          $p = xml_parse($this->parser, $data);
 141  
 142          if ($p == 0) {
 143              // Parse failed
 144              $errcode = xml_get_error_code($this->parser);
 145              $errstring = xml_error_string($errcode);
 146              $lineno = xml_get_current_line_number($this->parser);
 147              if ($lineno !== false) {
 148                  $error = array('lineno' => $lineno);
 149                  $lineno--; // Line numbering starts at 1.
 150                  while ($lineno > 0) {
 151                      $data = strstr($data, "\n");
 152                      $lineno--;
 153                  }
 154                  $data .= "\n"; // In case there's only one line (no newline)
 155                  $line = substr($data, 0, strpos($data, "\n"));
 156                  $error['code']   = $errcode;
 157                  $error['string'] = $errstring;
 158                  $error['line']   = $line;
 159                  $this->error[] = $error;
 160              } else {
 161                  $this->error[] = array('code' => $errcode, 'string' => $errstring);
 162              }
 163          }
 164  
 165          if (!empty($this->remoteerror)) {
 166              return false;
 167          }
 168  
 169          if (count($this->cipher) > 0) {
 170              $this->cipher = array_values($this->cipher);
 171              $this->payload_encrypted = true;
 172          }
 173  
 174          return (bool)$p;
 175      }
 176  
 177      /**
 178       * Destroy the parser and free up any related resource.
 179       */
 180      function free_resource() {
 181          $free = xml_parser_free($this->parser);
 182      }
 183  
 184      /**
 185       * Set the character-data handler to the right function for each element
 186       *
 187       * For each tag (element) name, this function switches the character-data
 188       * handler to the function that handles that element. Note that character
 189       * data is referred to the handler in blocks of 1024 bytes.
 190       *
 191       * @param   mixed   $parser The XML parser
 192       * @param   string  $name   The name of the tag, e.g. method_call
 193       * @param   array   $attrs  The tag's attributes (if any exist).
 194       * @return  bool            True
 195       */
 196      function start_element($parser, $name, $attrs) {
 197          $this->tag_number++;
 198          $handler = 'discard_data';
 199          switch(strtoupper($name)) {
 200              case 'DIGESTVALUE':
 201                  $handler = 'parse_digest';
 202                  break;
 203              case 'SIGNATUREVALUE':
 204                  $handler = 'parse_signature';
 205                  break;
 206              case 'OBJECT':
 207                  $handler = 'parse_object';
 208                  break;
 209              case 'RETRIEVALMETHOD':
 210                  $this->key_URI = $attrs['URI'];
 211                  break;
 212              case 'TIMESTAMP':
 213                  $handler = 'parse_timestamp';
 214                  break;
 215              case 'WWWROOT':
 216                  $handler = 'parse_wwwroot';
 217                  break;
 218              case 'CIPHERVALUE':
 219                  $this->cipher[$this->tag_number] = '';
 220                  $handler = 'parse_cipher';
 221                  break;
 222              case 'FAULT':
 223                  $handler = 'parse_fault';
 224              default:
 225                  break;
 226          }
 227          xml_set_character_data_handler($this->parser, $handler);
 228          return true;
 229      }
 230  
 231      /**
 232       * Add the next chunk of character data to the remote_timestamp string
 233       *
 234       * @param   mixed   $parser The XML parser
 235       * @param   string  $data   The content of the current tag (1024 byte chunk)
 236       * @return  bool            True
 237       */
 238      function parse_timestamp($parser, $data) {
 239          $this->remote_timestamp .= $data;
 240          return true;
 241      }
 242  
 243      /**
 244       * Add the next chunk of character data to the cipher string for that tag
 245       *
 246       * The XML parser calls the character-data handler with 1024-character
 247       * chunks of data. This means that the handler may be called several times
 248       * for a single tag, so we use the concatenate operator (.) to build the
 249       * tag content into a string.
 250       * We should not encounter more than one of each tag type, except for the
 251       * cipher tag. We will often see two of those. We prevent the content of
 252       * these two tags being concatenated together by counting each tag, and
 253       * using its 'number' as the key to an array of ciphers.
 254       *
 255       * @param   mixed   $parser The XML parser
 256       * @param   string  $data   The content of the current tag (1024 byte chunk)
 257       * @return  bool            True
 258       */
 259      function parse_cipher($parser, $data) {
 260          $this->cipher[$this->tag_number] .= $data;
 261          return true;
 262      }
 263  
 264      /**
 265       * Add the next chunk of character data to the remote_wwwroot string
 266       *
 267       * @param   mixed   $parser The XML parser
 268       * @param   string  $data   The content of the current tag (1024 byte chunk)
 269       * @return  bool            True
 270       */
 271      function parse_wwwroot($parser, $data) {
 272          $this->remote_wwwroot .= $data;
 273          return true;
 274      }
 275  
 276      /**
 277       * Add the next chunk of character data to the digest string
 278       *
 279       * @param   mixed   $parser The XML parser
 280       * @param   string  $data   The content of the current tag (1024 byte chunk)
 281       * @return  bool            True
 282       */
 283      function parse_digest($parser, $data) {
 284          $this->digest .= $data;
 285          return true;
 286      }
 287  
 288      /**
 289       * Add the next chunk of character data to the signature string
 290       *
 291       * @param   mixed   $parser The XML parser
 292       * @param   string  $data   The content of the current tag (1024 byte chunk)
 293       * @return  bool            True
 294       */
 295      function parse_signature($parser, $data) {
 296          $this->signature .= $data;
 297          return true;
 298      }
 299  
 300      /**
 301       * Add the next chunk of character data to the data_object string
 302       *
 303       * @param   mixed   $parser The XML parser
 304       * @param   string  $data   The content of the current tag (1024 byte chunk)
 305       * @return  bool            True
 306       */
 307      function parse_object($parser, $data) {
 308          $this->data_object .= $data;
 309          return true;
 310      }
 311  
 312      /**
 313       * Discard the next chunk of character data
 314       *
 315       * This is used for tags that we're not interested in.
 316       *
 317       * @param   mixed   $parser The XML parser
 318       * @param   string  $data   The content of the current tag (1024 byte chunk)
 319       * @return  bool            True
 320       */
 321      function discard_data($parser, $data) {
 322          if (!$this->errorstarted) {
 323              // Not interested
 324              return true;
 325          }
 326          $data = trim($data);
 327          if (isset($this->errorstarted->faultstringstarted) && !empty($data)) {
 328              $this->remoteerror .= ', message: ' . $data;
 329          } else if (isset($this->errorstarted->faultcodestarted)) {
 330              $this->remoteerror = 'code: ' . $data;
 331              unset($this->errorstarted->faultcodestarted);
 332          } else if ($data == 'faultCode') {
 333              $this->errorstarted->faultcodestarted = true;
 334          } else if ($data == 'faultString') {
 335              $this->errorstarted->faultstringstarted = true;
 336          }
 337          return true;
 338  
 339      }
 340  
 341      function parse_fault($parser, $data) {
 342          $this->errorstarted = new StdClass;
 343          return true;
 344      }
 345  
 346      /**
 347       * Switch the character-data handler to ignore the next chunk of data
 348       *
 349       * @param   mixed   $parser The XML parser
 350       * @param   string  $name   The name of the tag, e.g. method_call
 351       * @return  bool            True
 352       */
 353      function end_element($parser, $name) {
 354          $ok = xml_set_character_data_handler($this->parser, "discard_data");
 355          return true;
 356      }
 357  }