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