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