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