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