1<?php 2namespace IXR\Message; 3 4 5use IXR\DataType\Date; 6 7class Message 8{ 9 public $message; 10 public $messageType; // methodCall / methodResponse / fault 11 public $faultCode; 12 public $faultString; 13 public $methodName; 14 public $params; 15 16 // Current variable stacks 17 private $_arraystructs = []; // The stack used to keep track of the current array/struct 18 private $_arraystructstypes = []; // Stack keeping track of if things are structs or array 19 private $_currentStructName = []; // A stack as well 20 private $_param; 21 private $_value; 22 private $_currentTag; 23 private $_currentTagContents; 24 // The XML parser 25 private $_parser; 26 27 public function __construct($message) 28 { 29 $this->message =& $message; 30 } 31 32 public function parse() 33 { 34 // first remove the XML declaration 35 // merged from WP #10698 - this method avoids the RAM usage of preg_replace on very large messages 36 $header = preg_replace('/<\?xml.*?\?' . '>/s', '', substr($this->message, 0, 100), 1); 37 $this->message = trim(substr_replace($this->message, $header, 0, 100)); 38 if ('' == $this->message) { 39 return false; 40 } 41 42 // Then remove the DOCTYPE 43 $header = preg_replace('/^<!DOCTYPE[^>]*+>/i', '', substr($this->message, 0, 200), 1); 44 $this->message = trim(substr_replace($this->message, $header, 0, 200)); 45 if ('' == $this->message) { 46 return false; 47 } 48 49 // Check that the root tag is valid 50 $root_tag = substr($this->message, 0, strcspn(substr($this->message, 0, 20), "> \t\r\n")); 51 if ('<!DOCTYPE' === strtoupper($root_tag)) { 52 return false; 53 } 54 if (!in_array($root_tag, ['<methodCall', '<methodResponse', '<fault'])) { 55 return false; 56 } 57 58 // Bail if there are too many elements to parse 59 $element_limit = 30000; 60 if ($element_limit && 2 * $element_limit < substr_count($this->message, '<')) { 61 return false; 62 } 63 64 $this->_parser = xml_parser_create(); 65 // Set XML parser to take the case of tags in to account 66 xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, false); 67 // Set XML parser callback functions 68 xml_set_element_handler($this->_parser, [$this, 'tagOpen'], [$this, 'tagClose']); 69 xml_set_character_data_handler($this->_parser, [$this, 'cdata']); 70 $chunk_size = 262144; // 256Kb, parse in chunks to avoid the RAM usage on very large messages 71 $final = false; 72 do { 73 if (strlen($this->message) <= $chunk_size) { 74 $final = true; 75 } 76 $part = substr($this->message, 0, $chunk_size); 77 $this->message = substr($this->message, $chunk_size); 78 if (!xml_parse($this->_parser, $part, $final)) { 79 return false; 80 } 81 if ($final) { 82 break; 83 } 84 } while (true); 85 if(PHP_VERSION_ID < 80000) { 86 // Manually freeing the XML parser is only necessary in PHP versions prior to 8.0 87 xml_parser_free($this->_parser); 88 unset($this->_parser); // release the reference to the parser 89 } 90 91 // Grab the error messages, if any 92 if ($this->messageType === 'fault') { 93 $this->faultCode = $this->params[0]['faultCode']; 94 $this->faultString = $this->params[0]['faultString']; 95 } 96 return true; 97 } 98 99 /** 100 * Opening tag handler 101 * @param $parser 102 * @param $tag 103 * @param $attr 104 */ 105 public function tagOpen($parser, $tag, $attr) 106 { 107 $this->_currentTagContents = ''; 108 $this->_currentTag = $tag; 109 switch ($tag) { 110 case 'methodCall': 111 case 'methodResponse': 112 case 'fault': 113 $this->messageType = $tag; 114 break; 115 /* Deal with stacks of arrays and structs */ 116 case 'data': // data is to all intents and purposes more interesting than array 117 $this->_arraystructstypes[] = 'array'; 118 $this->_arraystructs[] = []; 119 break; 120 case 'struct': 121 $this->_arraystructstypes[] = 'struct'; 122 $this->_arraystructs[] = []; 123 break; 124 } 125 } 126 127 /** 128 * Character Data handler 129 * @param $parser 130 * @param $cdata 131 */ 132 public function cdata($parser, $cdata) 133 { 134 $this->_currentTagContents .= $cdata; 135 } 136 137 /** 138 * Closing tag handler 139 * @param $parser 140 * @param $tag 141 */ 142 public function tagClose($parser, $tag) 143 { 144 $valueFlag = false; 145 switch ($tag) { 146 case 'int': 147 case 'i4': 148 $value = (int)trim($this->_currentTagContents); 149 $valueFlag = true; 150 break; 151 case 'double': 152 $value = (float)trim($this->_currentTagContents); 153 $valueFlag = true; 154 break; 155 case 'string': 156 $value = (string)($this->_currentTagContents); 157 $valueFlag = true; 158 break; 159 case 'dateTime.iso8601': 160 $value = new Date(trim($this->_currentTagContents)); 161 $valueFlag = true; 162 break; 163 case 'value': 164 // "If no type is indicated, the type is string." 165 if (trim($this->_currentTagContents) != '') { 166 $value = (string)$this->_currentTagContents; 167 $valueFlag = true; 168 } 169 break; 170 case 'boolean': 171 $value = (bool)trim($this->_currentTagContents); 172 $valueFlag = true; 173 break; 174 case 'base64': 175 $value = base64_decode($this->_currentTagContents); 176 $valueFlag = true; 177 break; 178 /* Deal with stacks of arrays and structs */ 179 case 'data': 180 case 'struct': 181 $value = array_pop($this->_arraystructs); 182 array_pop($this->_arraystructstypes); 183 $valueFlag = true; 184 break; 185 case 'member': 186 array_pop($this->_currentStructName); 187 break; 188 case 'name': 189 $this->_currentStructName[] = trim($this->_currentTagContents); 190 break; 191 case 'methodName': 192 $this->methodName = trim($this->_currentTagContents); 193 break; 194 } 195 196 if ($valueFlag) { 197 if (count($this->_arraystructs) > 0) { 198 // Add value to struct or array 199 if ($this->_arraystructstypes[count($this->_arraystructstypes) - 1] === 'struct') { 200 // Add to struct 201 $this->_arraystructs[count($this->_arraystructs) - 1][$this->_currentStructName[count($this->_currentStructName) - 1]] = $value; 202 } else { 203 // Add to array 204 $this->_arraystructs[count($this->_arraystructs) - 1][] = $value; 205 } 206 } else { 207 // Just add as a parameter 208 $this->params[] = $value; 209 } 210 } 211 $this->_currentTagContents = ''; 212 } 213} 214