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