1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\Json; 11 12use SimpleXMLElement; 13use Zend\Json\Exception\RecursionException; 14use Zend\Json\Exception\RuntimeException; 15use ZendXml\Security as XmlSecurity; 16 17/** 18 * Class for encoding to and decoding from JSON. 19 */ 20class Json 21{ 22 /** 23 * How objects should be encoded -- arrays or as stdClass. TYPE_ARRAY is 1 24 * so that it is a boolean true value, allowing it to be used with 25 * ext/json's functions. 26 */ 27 const TYPE_ARRAY = 1; 28 const TYPE_OBJECT = 0; 29 30 /** 31 * To check the allowed nesting depth of the XML tree during xml2json conversion. 32 * 33 * @var int 34 */ 35 public static $maxRecursionDepthAllowed = 25; 36 37 /** 38 * @var bool 39 */ 40 public static $useBuiltinEncoderDecoder = false; 41 42 /** 43 * Decodes the given $encodedValue string which is 44 * encoded in the JSON format 45 * 46 * Uses ext/json's json_decode if available. 47 * 48 * @param string $encodedValue Encoded in JSON format 49 * @param int $objectDecodeType Optional; flag indicating how to decode 50 * objects. See {@link Zend\Json\Decoder::decode()} for details. 51 * @return mixed 52 * @throws RuntimeException 53 */ 54 public static function decode($encodedValue, $objectDecodeType = self::TYPE_OBJECT) 55 { 56 $encodedValue = (string) $encodedValue; 57 if (function_exists('json_decode') && static::$useBuiltinEncoderDecoder !== true) { 58 $decode = json_decode($encodedValue, $objectDecodeType); 59 60 switch (json_last_error()) { 61 case JSON_ERROR_NONE: 62 break; 63 case JSON_ERROR_DEPTH: 64 throw new RuntimeException('Decoding failed: Maximum stack depth exceeded'); 65 case JSON_ERROR_CTRL_CHAR: 66 throw new RuntimeException('Decoding failed: Unexpected control character found'); 67 case JSON_ERROR_SYNTAX: 68 throw new RuntimeException('Decoding failed: Syntax error'); 69 default: 70 throw new RuntimeException('Decoding failed'); 71 } 72 73 return $decode; 74 } 75 76 return Decoder::decode($encodedValue, $objectDecodeType); 77 } 78 79 /** 80 * Encode the mixed $valueToEncode into the JSON format 81 * 82 * Encodes using ext/json's json_encode() if available. 83 * 84 * NOTE: Object should not contain cycles; the JSON format 85 * does not allow object reference. 86 * 87 * NOTE: Only public variables will be encoded 88 * 89 * NOTE: Encoding native javascript expressions are possible using Zend\Json\Expr. 90 * You can enable this by setting $options['enableJsonExprFinder'] = true 91 * 92 * @see Zend\Json\Expr 93 * 94 * @param mixed $valueToEncode 95 * @param bool $cycleCheck Optional; whether or not to check for object recursion; off by default 96 * @param array $options Additional options used during encoding 97 * @return string JSON encoded object 98 */ 99 public static function encode($valueToEncode, $cycleCheck = false, $options = array()) 100 { 101 if (is_object($valueToEncode)) { 102 if (method_exists($valueToEncode, 'toJson')) { 103 return $valueToEncode->toJson(); 104 } elseif (method_exists($valueToEncode, 'toArray')) { 105 return static::encode($valueToEncode->toArray(), $cycleCheck, $options); 106 } 107 } 108 109 // Pre-encoding look for Zend\Json\Expr objects and replacing by tmp ids 110 $javascriptExpressions = array(); 111 if (isset($options['enableJsonExprFinder']) 112 && ($options['enableJsonExprFinder'] == true) 113 ) { 114 $valueToEncode = static::_recursiveJsonExprFinder($valueToEncode, $javascriptExpressions); 115 } 116 117 $prettyPrint = (isset($options['prettyPrint']) && ($options['prettyPrint'] == true)); 118 119 // Encoding 120 if (function_exists('json_encode') && static::$useBuiltinEncoderDecoder !== true) { 121 $encodeOptions = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP; 122 123 if ($prettyPrint && defined('JSON_PRETTY_PRINT')) { 124 $encodeOptions |= JSON_PRETTY_PRINT; 125 $prettyPrint = false; 126 } 127 128 $encodedResult = json_encode( 129 $valueToEncode, 130 $encodeOptions 131 ); 132 } else { 133 $encodedResult = Encoder::encode($valueToEncode, $cycleCheck, $options); 134 } 135 136 if ($prettyPrint) { 137 $encodedResult = self::prettyPrint($encodedResult, array("intent" => " ")); 138 } 139 140 //only do post-processing to revert back the Zend\Json\Expr if any. 141 if (count($javascriptExpressions) > 0) { 142 $count = count($javascriptExpressions); 143 for ($i = 0; $i < $count; $i++) { 144 $magicKey = $javascriptExpressions[$i]['magicKey']; 145 $value = $javascriptExpressions[$i]['value']; 146 147 $encodedResult = str_replace( 148 //instead of replacing "key:magicKey", we replace directly magicKey by value because "key" never changes. 149 '"' . $magicKey . '"', 150 $value, 151 $encodedResult 152 ); 153 } 154 } 155 156 return $encodedResult; 157 } 158 159 /** 160 * Check & Replace Zend\Json\Expr for tmp ids in the valueToEncode 161 * 162 * Check if the value is a Zend\Json\Expr, and if replace its value 163 * with a magic key and save the javascript expression in an array. 164 * 165 * NOTE this method is recursive. 166 * 167 * NOTE: This method is used internally by the encode method. 168 * 169 * @see encode 170 * @param mixed $value a string - object property to be encoded 171 * @param array $javascriptExpressions 172 * @param null|string|int $currentKey 173 * @return mixed 174 */ 175 protected static function _recursiveJsonExprFinder( 176 &$value, 177 array &$javascriptExpressions, 178 $currentKey = null 179 ) { 180 if ($value instanceof Expr) { 181 // TODO: Optimize with ascii keys, if performance is bad 182 $magicKey = "____" . $currentKey . "_" . (count($javascriptExpressions)); 183 $javascriptExpressions[] = array( 184 185 //if currentKey is integer, encodeUnicodeString call is not required. 186 "magicKey" => (is_int($currentKey)) ? $magicKey : Encoder::encodeUnicodeString($magicKey), 187 "value" => $value->__toString(), 188 ); 189 $value = $magicKey; 190 } elseif (is_array($value)) { 191 foreach ($value as $k => $v) { 192 $value[$k] = static::_recursiveJsonExprFinder($value[$k], $javascriptExpressions, $k); 193 } 194 } elseif (is_object($value)) { 195 foreach ($value as $k => $v) { 196 $value->$k = static::_recursiveJsonExprFinder($value->$k, $javascriptExpressions, $k); 197 } 198 } 199 return $value; 200 } 201 /** 202 * Return the value of an XML attribute text or the text between 203 * the XML tags 204 * 205 * In order to allow Zend\Json\Expr from xml, we check if the node 206 * matches the pattern that try to detect if it is a new Zend\Json\Expr 207 * if it matches, we return a new Zend\Json\Expr instead of a text node 208 * 209 * @param SimpleXMLElement $simpleXmlElementObject 210 * @return Expr|string 211 */ 212 protected static function _getXmlValue($simpleXmlElementObject) 213 { 214 $pattern = '/^[\s]*new Zend[_\\]Json[_\\]Expr[\s]*\([\s]*[\"\']{1}(.*)[\"\']{1}[\s]*\)[\s]*$/'; 215 $matchings = array(); 216 $match = preg_match($pattern, $simpleXmlElementObject, $matchings); 217 if ($match) { 218 return new Expr($matchings[1]); 219 } 220 return (trim(strval($simpleXmlElementObject))); 221 } 222 223 /** 224 * _processXml - Contains the logic for xml2json 225 * 226 * The logic in this function is a recursive one. 227 * 228 * The main caller of this function (i.e. fromXml) needs to provide 229 * only the first two parameters i.e. the SimpleXMLElement object and 230 * the flag for ignoring or not ignoring XML attributes. The third parameter 231 * will be used internally within this function during the recursive calls. 232 * 233 * This function converts the SimpleXMLElement object into a PHP array by 234 * calling a recursive (protected static) function in this class. Once all 235 * the XML elements are stored in the PHP array, it is returned to the caller. 236 * 237 * @param SimpleXMLElement $simpleXmlElementObject 238 * @param bool $ignoreXmlAttributes 239 * @param int $recursionDepth 240 * @throws Exception\RecursionException if the XML tree is deeper than the allowed limit. 241 * @return array 242 */ 243 protected static function _processXml($simpleXmlElementObject, $ignoreXmlAttributes, $recursionDepth = 0) 244 { 245 // Keep an eye on how deeply we are involved in recursion. 246 if ($recursionDepth > static::$maxRecursionDepthAllowed) { 247 // XML tree is too deep. Exit now by throwing an exception. 248 throw new RecursionException( 249 "Function _processXml exceeded the allowed recursion depth of " 250 . static::$maxRecursionDepthAllowed 251 ); 252 } 253 254 $children = $simpleXmlElementObject->children(); 255 $name = $simpleXmlElementObject->getName(); 256 $value = static::_getXmlValue($simpleXmlElementObject); 257 $attributes = (array) $simpleXmlElementObject->attributes(); 258 259 if (!count($children)) { 260 if (!empty($attributes) && !$ignoreXmlAttributes) { 261 foreach ($attributes['@attributes'] as $k => $v) { 262 $attributes['@attributes'][$k] = static::_getXmlValue($v); 263 } 264 if (!empty($value)) { 265 $attributes['@text'] = $value; 266 } 267 return array($name => $attributes); 268 } 269 270 return array($name => $value); 271 } 272 273 $childArray = array(); 274 foreach ($children as $child) { 275 $childname = $child->getName(); 276 $element = static::_processXml($child, $ignoreXmlAttributes, $recursionDepth + 1); 277 if (array_key_exists($childname, $childArray)) { 278 if (empty($subChild[$childname])) { 279 $childArray[$childname] = array($childArray[$childname]); 280 $subChild[$childname] = true; 281 } 282 $childArray[$childname][] = $element[$childname]; 283 } else { 284 $childArray[$childname] = $element[$childname]; 285 } 286 } 287 288 if (!empty($attributes) && !$ignoreXmlAttributes) { 289 foreach ($attributes['@attributes'] as $k => $v) { 290 $attributes['@attributes'][$k] = static::_getXmlValue($v); 291 } 292 $childArray['@attributes'] = $attributes['@attributes']; 293 } 294 295 if (!empty($value)) { 296 $childArray['@text'] = $value; 297 } 298 299 return array($name => $childArray); 300 } 301 302 /** 303 * @deprecated by https://github.com/zendframework/zf2/pull/6778 304 * fromXml - Converts XML to JSON 305 * 306 * Converts a XML formatted string into a JSON formatted string. 307 * The value returned will be a string in JSON format. 308 * 309 * The caller of this function needs to provide only the first parameter, 310 * which is an XML formatted String. The second parameter is optional, which 311 * lets the user to select if the XML attributes in the input XML string 312 * should be included or ignored in xml2json conversion. 313 * 314 * This function converts the XML formatted string into a PHP array by 315 * calling a recursive (protected static) function in this class. Then, it 316 * converts that PHP array into JSON by calling the "encode" static function. 317 * 318 * NOTE: Encoding native javascript expressions via Zend\Json\Expr is not possible. 319 * 320 * @static 321 * @access public 322 * @param string $xmlStringContents XML String to be converted 323 * @param bool $ignoreXmlAttributes Include or exclude XML attributes in 324 * the xml2json conversion process. 325 * @return mixed - JSON formatted string on success 326 * @throws \Zend\Json\Exception\RuntimeException if the input not a XML formatted string 327 */ 328 public static function fromXml($xmlStringContents, $ignoreXmlAttributes = true) 329 { 330 // Load the XML formatted string into a Simple XML Element object. 331 $simpleXmlElementObject = XmlSecurity::scan($xmlStringContents); 332 333 // If it is not a valid XML content, throw an exception. 334 if (!$simpleXmlElementObject) { 335 throw new RuntimeException('Function fromXml was called with an invalid XML formatted string.'); 336 } // End of if ($simpleXmlElementObject === null) 337 338 // Call the recursive function to convert the XML into a PHP array. 339 $resultArray = static::_processXml($simpleXmlElementObject, $ignoreXmlAttributes); 340 341 // Convert the PHP array to JSON using Zend\Json\Json encode method. 342 // It is just that simple. 343 $jsonStringOutput = static::encode($resultArray); 344 return($jsonStringOutput); 345 } 346 347 /** 348 * Pretty-print JSON string 349 * 350 * Use 'indent' option to select indentation string - by default it's a tab 351 * 352 * @param string $json Original JSON string 353 * @param array $options Encoding options 354 * @return string 355 */ 356 public static function prettyPrint($json, $options = array()) 357 { 358 $tokens = preg_split('|([\{\}\]\[,])|', $json, -1, PREG_SPLIT_DELIM_CAPTURE); 359 $result = ""; 360 $indent = 0; 361 362 $ind = " "; 363 if (isset($options['indent'])) { 364 $ind = $options['indent']; 365 } 366 367 $inLiteral = false; 368 foreach ($tokens as $token) { 369 $token = trim($token); 370 if ($token == "") { 371 continue; 372 } 373 374 if (preg_match('/^("(?:.*)"):[ ]?(.*)$/', $token, $matches)) { 375 $token = $matches[1] . ': ' . $matches[2]; 376 } 377 378 $prefix = str_repeat($ind, $indent); 379 if (!$inLiteral && ($token == "{" || $token == "[")) { 380 $indent++; 381 if ($result != "" && $result[strlen($result)-1] == "\n") { 382 $result .= $prefix; 383 } 384 $result .= "$token\n"; 385 } elseif (!$inLiteral && ($token == "}" || $token == "]")) { 386 $indent--; 387 $prefix = str_repeat($ind, $indent); 388 $result .= "\n$prefix$token"; 389 } elseif (!$inLiteral && $token == ",") { 390 $result .= "$token\n"; 391 } else { 392 $result .= ($inLiteral ? '' : $prefix) . $token; 393 394 //remove escaped backslash sequences causing false positives in next check 395 $token = str_replace('\\', '', $token); 396 // Count # of unescaped double-quotes in token, subtract # of 397 // escaped double-quotes and if the result is odd then we are 398 // inside a string literal 399 if ((substr_count($token, '"')-substr_count($token, '\\"')) % 2 != 0) { 400 $inLiteral = !$inLiteral; 401 } 402 } 403 } 404 return $result; 405 } 406} 407