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 xml_parser_free($this->_parser); 86 87 // Grab the error messages, if any 88 if ($this->messageType === 'fault') { 89 $this->faultCode = $this->params[0]['faultCode']; 90 $this->faultString = $this->params[0]['faultString']; 91 } 92 return true; 93 } 94 95 /** 96 * Opening tag handler 97 * @param $parser 98 * @param $tag 99 * @param $attr 100 */ 101 public function tagOpen($parser, $tag, $attr) 102 { 103 $this->_currentTagContents = ''; 104 $this->_currentTag = $tag; 105 switch ($tag) { 106 case 'methodCall': 107 case 'methodResponse': 108 case 'fault': 109 $this->messageType = $tag; 110 break; 111 /* Deal with stacks of arrays and structs */ 112 case 'data': // data is to all intents and purposes more interesting than array 113 $this->_arraystructstypes[] = 'array'; 114 $this->_arraystructs[] = []; 115 break; 116 case 'struct': 117 $this->_arraystructstypes[] = 'struct'; 118 $this->_arraystructs[] = []; 119 break; 120 } 121 } 122 123 /** 124 * Character Data handler 125 * @param $parser 126 * @param $cdata 127 */ 128 public function cdata($parser, $cdata) 129 { 130 $this->_currentTagContents .= $cdata; 131 } 132 133 /** 134 * Closing tag handler 135 * @param $parser 136 * @param $tag 137 */ 138 public function tagClose($parser, $tag) 139 { 140 $valueFlag = false; 141 switch ($tag) { 142 case 'int': 143 case 'i4': 144 $value = (int)trim($this->_currentTagContents); 145 $valueFlag = true; 146 break; 147 case 'double': 148 $value = (double)trim($this->_currentTagContents); 149 $valueFlag = true; 150 break; 151 case 'string': 152 $value = (string)($this->_currentTagContents); 153 $valueFlag = true; 154 break; 155 case 'dateTime.iso8601': 156 $value = new Date(trim($this->_currentTagContents)); 157 $valueFlag = true; 158 break; 159 case 'value': 160 // "If no type is indicated, the type is string." 161 if (trim($this->_currentTagContents) != '') { 162 $value = (string)$this->_currentTagContents; 163 $valueFlag = true; 164 } 165 break; 166 case 'boolean': 167 $value = (boolean)trim($this->_currentTagContents); 168 $valueFlag = true; 169 break; 170 case 'base64': 171 $value = base64_decode($this->_currentTagContents); 172 $valueFlag = true; 173 break; 174 /* Deal with stacks of arrays and structs */ 175 case 'data': 176 case 'struct': 177 $value = array_pop($this->_arraystructs); 178 array_pop($this->_arraystructstypes); 179 $valueFlag = true; 180 break; 181 case 'member': 182 array_pop($this->_currentStructName); 183 break; 184 case 'name': 185 $this->_currentStructName[] = trim($this->_currentTagContents); 186 break; 187 case 'methodName': 188 $this->methodName = trim($this->_currentTagContents); 189 break; 190 } 191 192 if ($valueFlag) { 193 if (count($this->_arraystructs) > 0) { 194 // Add value to struct or array 195 if ($this->_arraystructstypes[count($this->_arraystructstypes) - 1] === 'struct') { 196 // Add to struct 197 $this->_arraystructs[count($this->_arraystructs) - 1][$this->_currentStructName[count($this->_currentStructName) - 1]] = $value; 198 } else { 199 // Add to array 200 $this->_arraystructs[count($this->_arraystructs) - 1][] = $value; 201 } 202 } else { 203 // Just add as a parameter 204 $this->params[] = $value; 205 } 206 } 207 $this->_currentTagContents = ''; 208 } 209} 210