1<?php
2/*
3** Zabbix
4** Copyright (C) 2001-2021 Zabbix SIA
5**
6** This program is free software; you can redistribute it and/or modify
7** it under the terms of the GNU General Public License as published by
8** the Free Software Foundation; either version 2 of the License, or
9** (at your option) any later version.
10**
11** This program is distributed in the hope that it will be useful,
12** but WITHOUT ANY WARRANTY; without even the implied warranty of
13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14** GNU General Public License for more details.
15**
16** You should have received a copy of the GNU General Public License
17** along with this program; if not, write to the Free Software
18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19**/
20
21
22/**
23** Class for wrapping JSON encoding/decoding functionality.
24**
25** @ MOD from package Solar_Json <solarphp.com>
26**
27** @author Michal Migurski <mike-json@teczno.com>
28** @author Matt Knapp <mdknapp[at]gmail[dot]com>
29** @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
30** @author Clay Loveless <clay@killersoft.com>
31** @modified by Artem Suharev <aly@zabbix.com>
32**
33** @license http://opensource.org/licenses/bsd-license.php BSD
34**/
35class CJson {
36
37	/**
38	 *
39	 * User-defined configuration, primarily of use in unit testing.
40	 *
41	 * Keys are ...
42	 *
43	 * `bypass_ext`
44	 * : (bool) Flag to instruct Solar_Json to bypass
45	 *   native json extension, ifinstalled.
46	 *
47	 * `bypass_mb`
48	 * : (bool) Flag to instruct Solar_Json to bypass
49	 *   native mb_convert_encoding() function, if
50	 *   installed.
51	 *
52	 * `noerror`
53	 * : (bool) Flag to instruct Solar_Json to return null
54	 *   for values it cannot encode rather than throwing
55	 *   an exceptions (PHP-only encoding) or PHP warnings
56	 *   (native json_encode() function).
57	 *
58	 * @var array
59	 *
60	 */
61	protected $_config = [
62		'bypass_ext' => false,
63		'bypass_mb' => false,
64		'noerror' => false
65	];
66
67	/**
68	 *
69	 * Marker constants for use in _json_decode()
70	 *
71	 * @constant
72	 *
73	 */
74	const SLICE  = 1;
75	const IN_STR = 2;
76	const IN_ARR = 3;
77	const IN_OBJ = 4;
78	const IN_CMT = 5;
79
80	/**
81	 *
82	 * Nest level counter for determining correct behavior of decoding string
83	 * representations of numbers and boolean values.
84	 *
85	 * @var int
86	 */
87	protected $_level;
88
89	/**
90	 * Last error of $this->decode() method.
91	 *
92	 * @var int
93	 */
94	protected $last_error;
95
96	/**
97	 *
98	 * Constructor.
99	 *
100	 * If the $config param is an array, it is merged with the class
101	 * config array and any values from the Solar.config.php file.
102	 *
103	 * The Solar.config.php values are inherited along class parent
104	 * lines; for example, all classes descending from Solar_Base use the
105	 * Solar_Base config file values until overridden.
106	 *
107	 * @param mixed $config User-defined configuration values.
108	 *
109	 */
110	public function __construct($config = null) {
111		$this->_mapAscii();
112		$this->_setStateTransitionTable();
113
114		$this->last_error = JSON_ERROR_NONE;
115	}
116
117	/**
118	 * Default destructor; does nothing other than provide a safe fallback
119	 * for calls to parent::__destruct().
120	 */
121	public function __destruct() {
122	}
123
124	/**
125	 * Used for fallback _json_encode().
126	 * If true then non-associative array is encoded as object.
127	 *
128	 * @var bool
129	 */
130	private $force_object = false;
131
132	/**
133	 * Used for fallback _json_encode().
134	 * If true then forward slashes are escaped.
135	 *
136	 * @var bool
137	 */
138	private $escape_slashes = true;
139
140	/**
141	 * Encodes the mixed $valueToEncode into JSON format.
142	 *
143	 * @param mixed  $valueToEncode    Value to be encoded into JSON format.
144	 * @param array  $deQuote          Array of keys whose values should **not** be quoted in encoded string.
145	 * @param bool   $force_object     Force all arrays to objects.
146	 * @param bool   $escape_slashes
147	 *
148	 * @return string JSON encoded value
149	 */
150	public function encode($valueToEncode, $deQuote = [], $force_object = false, $escape_slashes = true) {
151		if (!$this->_config['bypass_ext'] && function_exists('json_encode') && defined('JSON_FORCE_OBJECT')
152				&& defined('JSON_UNESCAPED_SLASHES')) {
153			if ($this->_config['noerror']) {
154				$old_errlevel = error_reporting(E_ERROR ^ E_WARNING);
155			}
156
157			$encoded = json_encode($valueToEncode,
158				($escape_slashes ? 0 : JSON_UNESCAPED_SLASHES) | ($force_object ? JSON_FORCE_OBJECT : 0)
159			);
160
161			if ($this->_config['noerror']) {
162				error_reporting($old_errlevel);
163			}
164		}
165		else {
166			// Fall back to php-only method.
167
168			$this->force_object = $force_object;
169			$this->escape_slashes = $escape_slashes;
170			$encoded = $this->_json_encode($valueToEncode);
171		}
172
173		// sometimes you just don't want some values quoted
174		if (!empty($deQuote)) {
175			$encoded = $this->_deQuote($encoded, $deQuote);
176		}
177
178		return $encoded;
179	}
180
181	/**
182	 *
183	 * Accepts a JSON-encoded string, and removes quotes around values of
184	 * keys specified in the $keys array.
185	 *
186	 * Sometimes, such as when constructing behaviors on the fly for "onSuccess"
187	 * handlers to an Ajax request, the value needs to **not** have quotes around
188	 * it. This method will remove those quotes and perform stripslashes on any
189	 * escaped quotes within the quoted value.
190	 *
191	 * @param string $encoded JSON-encoded string
192	 *
193	 * @param array $keys Array of keys whose values should be de-quoted
194	 *
195	 * @return string $encoded Cleaned string
196	 *
197	 */
198	protected function _deQuote($encoded, $keys) {
199		foreach ($keys as $key) {
200			$encoded = preg_replace_callback("/(\"".$key."\"\:)(\".*(?:[^\\\]\"))/U",
201					[$this, '_stripvalueslashes'], $encoded);
202		}
203		return $encoded;
204	}
205
206	/**
207	 *
208	 * Method for use with preg_replace_callback in the _deQuote() method.
209	 *
210	 * Returns \["keymatch":\]\[value\] where value has had its leading and
211	 * trailing double-quotes removed, and stripslashes() run on the rest of
212	 * the value.
213	 *
214	 * @param array $matches Regexp matches
215	 *
216	 * @return string replacement string
217	 *
218	 */
219	protected function _stripvalueslashes($matches) {
220		return $matches[1].stripslashes(substr($matches[2], 1, -1));
221	}
222
223	/**
224	 *
225	 * Decodes the $encodedValue string which is encoded in the JSON format.
226	 *
227	 * For compatibility with the native json_decode() function, this static
228	 * method accepts the $encodedValue string and an optional boolean value
229	 * $asArray which indicates whether or not the decoded value should be
230	 * returned as an array. The default is false, meaning the default return
231	 * from this method is an object.
232	 *
233	 * For compliance with the [JSON specification][], no attempt is made to
234	 * decode strings that are obviously not an encoded arrays or objects.
235	 *
236	 * [JSON specification]: http://www.ietf.org/rfc/rfc4627.txt
237	 *
238	 * @param string $encodedValue String encoded in JSON format
239	 *
240	 * @param bool $asArray Optional argument to decode as an array.
241	 * Default false.
242	 *
243	 * @return mixed decoded value
244	 *
245	 */
246	public function decode($encodedValue, $asArray = false) {
247		if (!$this->_config['bypass_ext'] && function_exists('json_decode') && function_exists('json_last_error')) {
248			$result = json_decode($encodedValue, $asArray);
249			$this->last_error = json_last_error();
250
251			return $result;
252		}
253
254		$first_char = substr(ltrim($encodedValue), 0, 1);
255
256		if ($first_char != '{' && $first_char != '[') {
257			$result = null;
258		}
259		else {
260			ini_set('pcre.backtrack_limit', '10000000');
261
262			$this->_level = 0;
263
264			$result = $this->isValid($encodedValue) ? $this->_json_decode($encodedValue, $asArray) : null;
265		}
266
267		$this->last_error = ($result === null) ? JSON_ERROR_SYNTAX : JSON_ERROR_NONE;
268
269		return $result;
270	}
271
272	/**
273	 * Returns true if last $this->decode call was with error.
274	 *
275	 * @return bool
276	 */
277	public function hasError() {
278		return ($this->last_error != JSON_ERROR_NONE);
279	}
280
281	/**
282	 *
283	 * Encodes the mixed $valueToEncode into the JSON format, without use of
284	 * native PHP json extension.
285	 *
286	 * @param mixed $var Any number, boolean, string, array, or object
287	 * to be encoded. Strings are expected to be in ASCII or UTF-8 format.
288	 *
289	 * @return mixed JSON string representation of input value
290	 *
291	 */
292	protected function _json_encode($var) {
293		switch (gettype($var)) {
294			case 'boolean':
295				return $var ? 'true' : 'false';
296			case 'NULL':
297				return 'null';
298			case 'integer':
299				// BREAK WITH Services_JSON:
300				// disabled for compatibility with ext/json. ext/json returns
301				// a string for integers, so we will to.
302				return (string) $var;
303			case 'double':
304			case 'float':
305				// BREAK WITH Services_JSON:
306				// disabled for compatibility with ext/json. ext/json returns
307				// a string for floats and doubles, so we will to.
308				return (string) $var;
309			case 'string':
310				// STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT
311				$ascii = '';
312				$strlen_var = strlen($var);
313
314				/*
315				 * Iterate over every character in the string,
316				 * escaping with a slash or encoding to UTF-8 where necessary
317				 */
318				for ($c = 0; $c < $strlen_var; ++$c) {
319					$ord_var_c = ord($var[$c]);
320					switch (true) {
321						case $ord_var_c == 0x08:
322							$ascii .= '\b';
323							break;
324						case $ord_var_c == 0x09:
325							$ascii .= '\t';
326							break;
327						case $ord_var_c == 0x0A:
328							$ascii .= '\n';
329							break;
330						case $ord_var_c == 0x0C:
331							$ascii .= '\f';
332							break;
333						case $ord_var_c == 0x0D:
334							$ascii .= '\r';
335							break;
336						case $ord_var_c == 0x22:
337							// falls through
338						case ($ord_var_c == 0x2F && $this->escape_slashes):
339							// falls through
340						case $ord_var_c == 0x5C:
341							// double quote, slash, slosh
342							$ascii .= '\\'.$var[$c];
343							break;
344						case ($ord_var_c >= 0x20 && $ord_var_c <= 0x7F):
345							// characters U-00000000 - U-0000007F (same as ASCII)
346							$ascii .= $var[$c];
347							break;
348						case (($ord_var_c & 0xE0) == 0xC0):
349							// characters U-00000080 - U-000007FF, mask 110XXXXX
350							// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
351							$char = pack('C*', $ord_var_c, ord($var[$c + 1]));
352							$c += 1;
353							$utf16 = $this->_utf82utf16($char);
354							$ascii .= sprintf('\u%04s', bin2hex($utf16));
355							break;
356						case (($ord_var_c & 0xF0) == 0xE0):
357							// characters U-00000800 - U-0000FFFF, mask 1110XXXX
358							// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
359							$char = pack('C*', $ord_var_c, ord($var[$c + 1]), ord($var[$c + 2]));
360							$c += 2;
361							$utf16 = $this->_utf82utf16($char);
362							$ascii .= sprintf('\u%04s', bin2hex($utf16));
363							break;
364						case (($ord_var_c & 0xF8) == 0xF0):
365							// characters U-00010000 - U-001FFFFF, mask 11110XXX
366							// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
367							$char = pack('C*', $ord_var_c, ord($var[$c + 1]), ord($var[$c + 2]), ord($var[$c + 3]));
368							$c += 3;
369							$utf16 = $this->_utf82utf16($char);
370							$ascii .= sprintf('\u%04s', bin2hex($utf16));
371							break;
372						case (($ord_var_c & 0xFC) == 0xF8):
373							// characters U-00200000 - U-03FFFFFF, mask 111110XX
374							// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
375							$char = pack('C*', $ord_var_c,
376										ord($var[$c + 1]),
377										ord($var[$c + 2]),
378										ord($var[$c + 3]),
379										ord($var[$c + 4]));
380							$c += 4;
381							$utf16 = $this->_utf82utf16($char);
382							$ascii .= sprintf('\u%04s', bin2hex($utf16));
383							break;
384						case (($ord_var_c & 0xFE) == 0xFC):
385							// characters U-04000000 - U-7FFFFFFF, mask 1111110X
386							// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
387							$char = pack('C*', $ord_var_c,
388										ord($var[$c + 1]),
389										ord($var[$c + 2]),
390										ord($var[$c + 3]),
391										ord($var[$c + 4]),
392										ord($var[$c + 5]));
393							$c += 5;
394							$utf16 = $this->_utf82utf16($char);
395							$ascii .= sprintf('\u%04s', bin2hex($utf16));
396							break;
397					}
398				}
399				return '"'.$ascii.'"';
400			case 'array':
401				/*
402				 * As per JSON spec if any array key is not an integer
403				 * we must treat the whole array as an object. We
404				 * also try to catch a sparsely populated associative
405				 * array with numeric keys here because some JS engines
406				 * will create an array with empty indexes up to
407				 * max_index which can cause memory issues and because
408				 * the keys, which may be relevant, will be remapped
409				 * otherwise.
410				 *
411				 * As per the ECMA and JSON specification an object may
412				 * have any string as a property. Unfortunately due to
413				 * a hole in the ECMA specification if the key is an
414				 * ECMA reserved word or starts with a digit the
415				 * parameter is only accessible using ECMAScript's
416				 * bracket notation.
417				 */
418
419				// treat as a JSON object
420				if ($this->force_object || is_array($var) && count($var)
421						&& array_keys($var) !== range(0, sizeof($var) - 1)) {
422					$properties = array_map([$this, '_name_value'], array_keys($var), array_values($var));
423					return '{' . join(',', $properties) . '}';
424				}
425
426				// treat it like a regular array
427				$elements = array_map([$this, '_json_encode'], $var);
428				return '[' . join(',', $elements) . ']';
429			case 'object':
430				$vars = get_object_vars($var);
431				$properties = array_map([$this, '_name_value'], array_keys($vars), array_values($vars));
432				return '{' . join(',', $properties) . '}';
433			default:
434				if ($this->_config['noerror']) {
435					return 'null';
436				}
437				throw Solar::exception(
438					'Solar_Json',
439					'ERR_CANNOT_ENCODE',
440					gettype($var).' cannot be encoded as a JSON string',
441					['var' => $var]
442				);
443		}
444	}
445
446	/**
447	 * Decodes a JSON string into appropriate variable.
448	 *
449	 * Note: several changes were made in translating this method from
450	 * Services_JSON, particularly related to how strings are handled. According
451	 * to JSON_checker test suite from <http://www.json.org/JSON_checker/>,
452	 * a JSON payload should be an object or an array, not a string.
453	 *
454	 * Therefore, returning bool(true) for 'true' is invalid JSON decoding
455	 * behavior, unless nested inside of an array or object.
456	 *
457	 * Similarly, a string of '1' should return null, not int(1), unless
458	 * nested inside of an array or object.
459	 *
460	 * @param string $str String encoded in JSON format
461	 * @param bool $asArray Optional argument to decode as an array.
462	 * @return mixed decoded value
463	 * @todo Rewrite this based off of method used in Solar_Json_Checker
464	 */
465	protected function _json_decode($str, $asArray = false) {
466		$str = $this->_reduce_string($str);
467
468		switch (strtolower($str)) {
469			case 'true':
470				// JSON_checker test suite claims
471				// "A JSON payload should be an object or array, not a string."
472				// Thus, returning bool(true) is invalid parsing, unless
473				// we're nested inside an array or object.
474				if (in_array($this->_level, [self::IN_ARR, self::IN_OBJ])) {
475					return true;
476				}
477				else {
478					return null;
479				}
480				break;
481			case 'false':
482				// JSON_checker test suite claims
483				// "A JSON payload should be an object or array, not a string."
484				// Thus, returning bool(false) is invalid parsing, unless
485				// we're nested inside an array or object.
486				if (in_array($this->_level, [self::IN_ARR, self::IN_OBJ])) {
487					return false;
488				}
489				else {
490					return null;
491				}
492				break;
493			case 'null':
494				return null;
495			default:
496				$m = [];
497
498				if (is_numeric($str) || ctype_digit($str) || ctype_xdigit($str)) {
499					// return float or int, or null as appropriate
500					if (in_array($this->_level, [self::IN_ARR, self::IN_OBJ])) {
501						return ((float) $str == (integer) $str) ? (integer) $str : (float) $str;
502					}
503					else {
504						return null;
505					}
506					break;
507				}
508				elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) {
509					// strings returned in UTF-8 format
510					$delim = substr($str, 0, 1);
511					$chrs = substr($str, 1, -1);
512					$utf8 = '';
513					$strlen_chrs = strlen($chrs);
514					for ($c = 0; $c < $strlen_chrs; ++$c) {
515						$substr_chrs_c_2 = substr($chrs, $c, 2);
516						$ord_chrs_c = ord($chrs[$c]);
517						switch (true) {
518							case $substr_chrs_c_2 == '\b':
519								$utf8 .= chr(0x08);
520								++$c;
521								break;
522							case $substr_chrs_c_2 == '\t':
523								$utf8 .= chr(0x09);
524								++$c;
525								break;
526							case $substr_chrs_c_2 == '\n':
527								$utf8 .= chr(0x0A);
528								++$c;
529								break;
530							case $substr_chrs_c_2 == '\f':
531								$utf8 .= chr(0x0C);
532								++$c;
533								break;
534							case $substr_chrs_c_2 == '\r':
535								$utf8 .= chr(0x0D);
536								++$c;
537								break;
538							case $substr_chrs_c_2 == '\\"':
539							case $substr_chrs_c_2 == '\\\'':
540							case $substr_chrs_c_2 == '\\\\':
541							case $substr_chrs_c_2 == '\\/':
542								if ($delim == '"' && $substr_chrs_c_2 != '\\\'' || $delim == "'"
543										&& $substr_chrs_c_2 != '\\"') {
544									$utf8 .= $chrs[++$c];
545								}
546								break;
547							case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)):
548								// single, escaped unicode character
549								$utf16 = chr(hexdec(substr($chrs, $c + 2, 2))).chr(hexdec(substr($chrs, $c + 4, 2)));
550								$utf8 .= $this->_utf162utf8($utf16);
551								$c += 5;
552								break;
553							case $ord_chrs_c >= 0x20 && $ord_chrs_c <= 0x7F:
554								$utf8 .= $chrs[$c];
555								break;
556							case ($ord_chrs_c & 0xE0) == 0xC0:
557								// characters U-00000080 - U-000007FF, mask 110XXXXX
558								// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
559								$utf8 .= substr($chrs, $c, 2);
560								++$c;
561								break;
562							case ($ord_chrs_c & 0xF0) == 0xE0:
563								// characters U-00000800 - U-0000FFFF, mask 1110XXXX
564								// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
565								$utf8 .= substr($chrs, $c, 3);
566								$c += 2;
567								break;
568							case ($ord_chrs_c & 0xF8) == 0xF0:
569								// characters U-00010000 - U-001FFFFF, mask 11110XXX
570								// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
571								$utf8 .= substr($chrs, $c, 4);
572								$c += 3;
573								break;
574							case ($ord_chrs_c & 0xFC) == 0xF8:
575								// characters U-00200000 - U-03FFFFFF, mask 111110XX
576								// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
577								$utf8 .= substr($chrs, $c, 5);
578								$c += 4;
579								break;
580							case ($ord_chrs_c & 0xFE) == 0xFC:
581								// characters U-04000000 - U-7FFFFFFF, mask 1111110X
582								// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
583								$utf8 .= substr($chrs, $c, 6);
584								$c += 5;
585								break;
586						}
587					}
588
589					if (in_array($this->_level, [self::IN_ARR, self::IN_OBJ])) {
590						return $utf8;
591					}
592					else {
593						return null;
594					}
595				}
596				elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) {
597					// array, or object notation
598					if ($str[0] == '[') {
599						$stk = [self::IN_ARR];
600						$this->_level = self::IN_ARR;
601						$arr = [];
602					}
603					else {
604						if ($asArray) {
605							$stk = [self::IN_OBJ];
606							$obj = [];
607						}
608						else {
609							$stk = [self::IN_OBJ];
610							$obj = new stdClass();
611						}
612						$this->_level = self::IN_OBJ;
613					}
614					array_push($stk, ['what' => self::SLICE, 'where' => 0, 'delim' => false]);
615
616					$chrs = substr($str, 1, -1);
617					$chrs = $this->_reduce_string($chrs);
618
619					if ($chrs == '') {
620						if (reset($stk) == self::IN_ARR) {
621							return $arr;
622						}
623						else {
624							return $obj;
625						}
626					}
627
628					$strlen_chrs = strlen($chrs);
629					for ($c = 0; $c <= $strlen_chrs; ++$c) {
630						$top = end($stk);
631						$substr_chrs_c_2 = substr($chrs, $c, 2);
632
633						if ($c == $strlen_chrs || ($chrs[$c] == ',' && $top['what'] == self::SLICE)) {
634							// found a comma that is not inside a string, array, etc.,
635							// OR we've reached the end of the character list
636							$slice = substr($chrs, $top['where'], $c - $top['where']);
637							array_push($stk, ['what' => self::SLICE, 'where' => $c + 1, 'delim' => false]);
638
639							if (reset($stk) == self::IN_ARR) {
640								$this->_level = self::IN_ARR;
641								// we are in an array, so just push an element onto the stack
642								array_push($arr, $this->_json_decode($slice, $asArray));
643							}
644							elseif (reset($stk) == self::IN_OBJ) {
645								$this->_level = self::IN_OBJ;
646								// we are in an object, so figure
647								// out the property name and set an
648								// element in an associative array,
649								// for now
650								$parts = [];
651
652								if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
653									// "name":value pair
654									$key = $this->_json_decode($parts[1], $asArray);
655									$val = $this->_json_decode($parts[2], $asArray);
656
657									if ($asArray) {
658										$obj[$key] = $val;
659									}
660									else {
661										$obj->$key = $val;
662									}
663								}
664								elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
665									// name:value pair, where name is unquoted
666									$key = $parts[1];
667									$val = $this->_json_decode($parts[2], $asArray);
668
669									if ($asArray) {
670										$obj[$key] = $val;
671									}
672									else {
673										$obj->$key = $val;
674									}
675								}
676								elseif (preg_match('/^\s*(["\']["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
677									// "":value pair
678									//$key = $this->_json_decode($parts[1]);
679									// use string that matches ext/json
680									$key = '_empty_';
681									$val = $this->_json_decode($parts[2], $asArray);
682
683									if ($asArray) {
684										$obj[$key] = $val;
685									}
686									else {
687										$obj->$key = $val;
688									}
689								}
690							}
691						}
692						elseif (($chrs[$c] == '"' || $chrs[$c] == "'") && $top['what'] != self::IN_STR) {
693							// found a quote, and we are not inside a string
694							array_push($stk, ['what' => self::IN_STR, 'where' => $c, 'delim' => $chrs[$c]]);
695						}
696						elseif (((strlen(substr($chrs, 0, $c)) - strlen(rtrim(substr($chrs, 0, $c), '\\'))) % 2 != 1)
697								&& $chrs[$c] == $top['delim'] && $top['what'] == self::IN_STR) {
698							// found a quote, we're in a string, and it's not escaped
699							// we know that it's not escaped because there is _not_ an
700							// odd number of backslashes at the end of the string so far
701							array_pop($stk);
702						}
703						elseif ($chrs[$c] == '['
704								&& in_array($top['what'], [self::SLICE, self::IN_ARR, self::IN_OBJ])) {
705							// found a left-bracket, and we are in an array, object, or slice
706							array_push($stk, ['what' => self::IN_ARR, 'where' => $c, 'delim' => false]);
707						}
708						elseif ($chrs[$c] == ']' && $top['what'] == self::IN_ARR) {
709							// found a right-bracket, and we're in an array
710							$this->_level = null;
711							array_pop($stk);
712						}
713						elseif ($chrs[$c] == '{'
714								&& in_array($top['what'], [self::SLICE, self::IN_ARR, self::IN_OBJ])) {
715							// found a left-brace, and we are in an array, object, or slice
716							array_push($stk, ['what' => self::IN_OBJ, 'where' => $c, 'delim' => false]);
717						}
718						elseif ($chrs[$c] == '}' && $top['what'] == self::IN_OBJ) {
719							// found a right-brace, and we're in an object
720							$this->_level = null;
721							array_pop($stk);
722						}
723						elseif ($substr_chrs_c_2 == '/*'
724								&& in_array($top['what'], [self::SLICE, self::IN_ARR, self::IN_OBJ])) {
725							// found a comment start, and we are in an array, object, or slice
726							array_push($stk, ['what' => self::IN_CMT, 'where' => $c, 'delim' => false]);
727							$c++;
728						}
729						elseif ($substr_chrs_c_2 == '*/' && ($top['what'] == self::IN_CMT)) {
730							// found a comment end, and we're in one now
731							array_pop($stk);
732							$c++;
733							for ($i = $top['where']; $i <= $c; ++$i) {
734								$chrs = substr_replace($chrs, ' ', $i, 1);
735							}
736						}
737					}
738
739					if (reset($stk) == self::IN_ARR) {
740						return $arr;
741					}
742					elseif (reset($stk) == self::IN_OBJ) {
743						return $obj;
744					}
745				}
746		}
747	}
748
749	/**
750	 * Array-walking method for use in generating JSON-formatted name-value
751	 * pairs in the form of '"name":value'.
752	 *
753	 * @param string $name name of key to use
754	 * @param mixed $value element to be encoded
755	 * @return string JSON-formatted name-value pair
756	 */
757	protected function _name_value($name, $value) {
758		$encoded_value = $this->_json_encode($value);
759		return $this->_json_encode(strval($name)) . ':' . $encoded_value;
760	}
761
762	/**
763	 * Convert a string from one UTF-16 char to one UTF-8 char.
764	 *
765	 * Normally should be handled by mb_convert_encoding, but
766	 * provides a slower PHP-only method for installations
767	 * that lack the multibye string extension.
768	 *
769	 * @param string $utf16 UTF-16 character
770	 * @return string UTF-8 character
771	 */
772	protected function _utf162utf8($utf16) {
773		// oh please oh please oh please oh please oh please
774		if (!$this->_config['bypass_mb'] && function_exists('mb_convert_encoding')) {
775			return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16');
776		}
777		$bytes = (ord($utf16[0]) << 8) | ord($utf16[1]);
778
779		switch (true) {
780			case ((0x7F & $bytes) == $bytes):
781				// this case should never be reached, because we are in ASCII range
782				// see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
783				return chr(0x7F & $bytes);
784			case (0x07FF & $bytes) == $bytes:
785				// return a 2-byte UTF-8 character
786				// see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
787				return chr(0xC0 | (($bytes >> 6) & 0x1F)).chr(0x80 | ($bytes & 0x3F));
788			case (0xFFFF & $bytes) == $bytes:
789				// return a 3-byte UTF-8 character
790				// see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
791				return chr(0xE0 | (($bytes >> 12) & 0x0F)).chr(0x80 | (($bytes >> 6) & 0x3F)).chr(0x80 | ($bytes & 0x3F));
792		}
793		// ignoring UTF-32 for now, sorry
794		return '';
795	}
796
797	/**
798	 * Convert a string from one UTF-8 char to one UTF-16 char.
799	 *
800	 * Normally should be handled by mb_convert_encoding, but
801	 * provides a slower PHP-only method for installations
802	 * that lack the multibye string extension.
803	 *
804	 * @param string $utf8 UTF-8 character
805	 * @return string UTF-16 character
806	 */
807	protected function _utf82utf16($utf8) {
808		// oh please oh please oh please oh please oh please
809		if (!$this->_config['bypass_mb'] && function_exists('mb_convert_encoding')) {
810			return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
811		}
812
813		switch (strlen($utf8)) {
814			case 1:
815				// this case should never be reached, because we are in ASCII range
816				// see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
817				return $utf8;
818			case 2:
819				// return a UTF-16 character from a 2-byte UTF-8 char
820				// see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
821				return chr(0x07 & (ord($utf8[0]) >> 2)).chr((0xC0 & (ord($utf8[0]) << 6)) | (0x3F & ord($utf8[1])));
822			case 3:
823				// return a UTF-16 character from a 3-byte UTF-8 char
824				// see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
825				return chr((0xF0 & (ord($utf8[0]) << 4)) | (0x0F & (ord($utf8[1]) >> 2))).
826						chr((0xC0 & (ord($utf8[1]) << 6)) | (0x7F & ord($utf8[2])));
827		}
828		// ignoring UTF-32 for now, sorry
829		return '';
830	}
831
832	/**
833	 * Reduce a string by removing leading and trailing comments and whitespace.
834	 *
835	 * @param string $str string value to strip of comments and whitespace
836	 * @return string string value stripped of comments and whitespace
837	 */
838	protected function _reduce_string($str) {
839		$str = preg_replace([
840			// eliminate single line comments in '// ...' form
841			'#^\s*//(.+)$#m',
842
843			// eliminate multi-line comments in '/* ... */' form, at start of string
844			'#^\s*/\*(.+)\*/#Us',
845
846			// eliminate multi-line comments in '/* ... */' form, at end of string
847			'#/\*(.+)\*/\s*$#Us'
848
849		], '', $str);
850		// eliminate extraneous space
851		return trim($str);
852	}
853
854	//***************************************************************************
855	// 								CHECK JSON									*
856	//***************************************************************************
857	const S_ERR = -1;	// Error
858	const S_SPA = 0;	// Space
859	const S_WSP = 1;	// Other whitespace
860	const S_LBE = 2;	// {
861	const S_RBE = 3;	// }
862	const S_LBT = 4;	// [
863	const S_RBT = 5;	// ]
864	const S_COL = 6;	// :
865	const S_COM = 7;	// ,
866	const S_QUO = 8;	// "
867	const S_BAC = 9;	// \
868	const S_SLA = 10;	// /
869	const S_PLU = 11;	// +
870	const S_MIN = 12;	// -
871	const S_DOT = 13;	// .
872	const S_ZER = 14;	// 0
873	const S_DIG = 15;	// 123456789
874	const S__A_ = 16;	// a
875	const S__B_ = 17;	// b
876	const S__C_ = 18;	// c
877	const S__D_ = 19;	// d
878	const S__E_ = 20;	// e
879	const S__F_ = 21;	// f
880	const S__L_ = 22;	// l
881	const S__N_ = 23;	// n
882	const S__R_ = 24;	// r
883	const S__S_ = 25;	// s
884	const S__T_ = 26;	// t
885	const S__U_ = 27;	// u
886	const S_A_F = 28;	// ABCDF
887	const S_E = 29;		// E
888	const S_ETC = 30;	// Everything else
889
890	/**
891	 * Map of 128 ASCII characters into the 32 character classes.
892	 * The remaining Unicode characters should be mapped to S_ETC.
893	 *
894	 * @var array
895	 */
896	protected $_ascii_class = [];
897
898	/**
899	 * State transition table.
900	 * @var array
901	 */
902	protected $_state_transition_table = [];
903
904	/**
905	 * These modes can be pushed on the "pushdown automata" (PDA) stack.
906	 * @constant
907	 */
908	const MODE_DONE		= 1;
909	const MODE_KEY		= 2;
910	const MODE_OBJECT	= 3;
911	const MODE_ARRAY	= 4;
912
913	/**
914	 * Max depth allowed for nested structures.
915	 * @constant
916	 */
917	const MAX_DEPTH = 20;
918
919	/**
920	 * The stack to maintain the state of nested structures.
921	 * @var array
922	 */
923	protected $_the_stack = [];
924
925	/**
926	 * Pointer for the top of the stack.
927	 * @var int
928	 */
929	protected $_the_top;
930
931	/**
932	 * The isValid method takes a UTF-16 encoded string and determines if it is
933	 * a syntactically correct JSON text.
934	 *
935	 * It is implemented as a Pushdown Automaton; that means it is a finite
936	 * state machine with a stack.
937	 *
938	 * @param string $str The JSON text to validate
939	 * @return bool
940	 */
941	public function isValid($str) {
942		$len = strlen($str);
943		$_the_state = 0;
944		$this->_the_top = -1;
945		$this->_push(self::MODE_DONE);
946
947		for ($_the_index = 0; $_the_index < $len; $_the_index++) {
948			$b = $str[$_the_index];
949			if (chr(ord($b) & 127) == $b) {
950				$c = $this->_ascii_class[ord($b)];
951				if ($c <= self::S_ERR) {
952					return false;
953				}
954			}
955			else {
956				$c = self::S_ETC;
957			}
958
959			// get the next state from the transition table
960			$s = $this->_state_transition_table[$_the_state][$c];
961
962			if ($s < 0) {
963				// perform one of the predefined actions
964				switch ($s) {
965					// empty }
966					case -9:
967						if (!$this->_pop(self::MODE_KEY)) {
968							return false;
969						}
970						$_the_state = 9;
971						break;
972					// {
973					case -8:
974						if (!$this->_push(self::MODE_KEY)) {
975							return false;
976						}
977						$_the_state = 1;
978						break;
979					// }
980					case -7:
981						if (!$this->_pop(self::MODE_OBJECT)) {
982							return false;
983						}
984						$_the_state = 9;
985						break;
986					// [
987					case -6:
988						if (!$this->_push(self::MODE_ARRAY)) {
989							return false;
990						}
991						$_the_state = 2;
992						break;
993					// ]
994					case -5:
995						if (!$this->_pop(self::MODE_ARRAY)) {
996							return false;
997						}
998						$_the_state = 9;
999						break;
1000					// "
1001					case -4:
1002						switch ($this->_the_stack[$this->_the_top]) {
1003							case self::MODE_KEY:
1004								$_the_state = 27;
1005								break;
1006							case self::MODE_ARRAY:
1007							case self::MODE_OBJECT:
1008								$_the_state = 9;
1009								break;
1010							default:
1011								return false;
1012						}
1013						break;
1014					// '
1015					case -3:
1016						switch ($this->_the_stack[$this->_the_top]) {
1017							case self::MODE_OBJECT:
1018								if ($this->_pop(self::MODE_OBJECT) && $this->_push(self::MODE_KEY)) {
1019									$_the_state = 29;
1020								}
1021								break;
1022							case self::MODE_ARRAY:
1023								$_the_state = 28;
1024								break;
1025							default:
1026								return false;
1027						}
1028						break;
1029					// :
1030					case -2:
1031						if ($this->_pop(self::MODE_KEY) && $this->_push(self::MODE_OBJECT)) {
1032							$_the_state = 28;
1033							break;
1034						}
1035					// syntax error
1036					case -1:
1037						return false;
1038				}
1039			}
1040			else {
1041				// change the state and iterate
1042				$_the_state = $s;
1043			}
1044		}
1045		if ($_the_state == 9 && $this->_pop(self::MODE_DONE)) {
1046			return true;
1047		}
1048		return false;
1049	}
1050
1051	/**
1052	 * Map the 128 ASCII characters into the 32 character classes.
1053	 * The remaining Unicode characters should be mapped to S_ETC.
1054	 */
1055	protected function _mapAscii() {
1056		$this->_ascii_class = [
1057			self::S_ERR, self::S_ERR, self::S_ERR, self::S_ERR, self::S_ERR, self::S_ERR, self::S_ERR, self::S_ERR,
1058			self::S_ERR, self::S_WSP, self::S_WSP, self::S_ERR, self::S_ERR, self::S_WSP, self::S_ERR, self::S_ERR,
1059			self::S_ERR, self::S_ERR, self::S_ERR, self::S_ERR, self::S_ERR, self::S_ERR, self::S_ERR, self::S_ERR,
1060			self::S_ERR, self::S_ERR, self::S_ERR, self::S_ERR, self::S_ERR, self::S_ERR, self::S_ERR, self::S_ERR,
1061
1062			self::S_SPA, self::S_ETC, self::S_QUO, self::S_ETC, self::S_ETC, self::S_ETC, self::S_ETC, self::S_ETC,
1063			self::S_ETC, self::S_ETC, self::S_ETC, self::S_PLU, self::S_COM, self::S_MIN, self::S_DOT, self::S_SLA,
1064			self::S_ZER, self::S_DIG, self::S_DIG, self::S_DIG, self::S_DIG, self::S_DIG, self::S_DIG, self::S_DIG,
1065			self::S_DIG, self::S_DIG, self::S_COL, self::S_ETC, self::S_ETC, self::S_ETC, self::S_ETC, self::S_ETC,
1066
1067			self::S_ETC, self::S_A_F, self::S_A_F, self::S_A_F, self::S_A_F, self::S_E  , self::S_A_F, self::S_ETC,
1068			self::S_ETC, self::S_ETC, self::S_ETC, self::S_ETC, self::S_ETC, self::S_ETC, self::S_ETC, self::S_ETC,
1069			self::S_ETC, self::S_ETC, self::S_ETC, self::S_ETC, self::S_ETC, self::S_ETC, self::S_ETC, self::S_ETC,
1070			self::S_ETC, self::S_ETC, self::S_ETC, self::S_LBT, self::S_BAC, self::S_RBT, self::S_ETC, self::S_ETC,
1071
1072			self::S_ETC, self::S__A_, self::S__B_, self::S__C_, self::S__D_, self::S__E_, self::S__F_, self::S_ETC,
1073			self::S_ETC, self::S_ETC, self::S_ETC, self::S_ETC, self::S__L_, self::S_ETC, self::S__N_, self::S_ETC,
1074			self::S_ETC, self::S_ETC, self::S__R_, self::S__S_, self::S__T_, self::S__U_, self::S_ETC, self::S_ETC,
1075			self::S_ETC, self::S_ETC, self::S_ETC, self::S_LBE, self::S_ETC, self::S_RBE, self::S_ETC, self::S_ETC
1076		];
1077	}
1078
1079	/**
1080	 * The state transition table takes the current state and the current symbol,
1081	 * and returns either a new state or an action. A new state is a number between
1082	 * 0 and 29. An action is a negative number between -1 and -9. A JSON text is
1083	 * accepted if the end of the text is in state 9 and mode is MODE_DONE.
1084	 */
1085	protected function _setStateTransitionTable() {
1086		$this->_state_transition_table = [
1087			[ 0, 0,-8,-1,-6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
1088			[ 1, 1,-1,-9,-1,-1,-1,-1, 3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
1089			[ 2, 2,-8,-1,-6,-5,-1,-1, 3,-1,-1,-1,20,-1,21,22,-1,-1,-1,-1,-1,13,-1,17,-1,-1,10,-1,-1,-1,-1],
1090			[ 3,-1, 3, 3, 3, 3, 3, 3,-4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
1091			[-1,-1,-1,-1,-1,-1,-1,-1, 3, 3, 3,-1,-1,-1,-1,-1,-1, 3,-1,-1,-1, 3,-1, 3, 3,-1, 3, 5,-1,-1,-1],
1092			[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 6, 6, 6, 6, 6, 6, 6, 6,-1,-1,-1,-1,-1,-1, 6, 6,-1],
1093			[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 7, 7, 7, 7, 7, 7, 7, 7,-1,-1,-1,-1,-1,-1, 7, 7,-1],
1094			[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 8, 8, 8, 8, 8, 8, 8, 8,-1,-1,-1,-1,-1,-1, 8, 8,-1],
1095			[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 3, 3, 3, 3, 3, 3, 3, 3,-1,-1,-1,-1,-1,-1, 3, 3,-1],
1096			[ 9, 9,-1,-7,-1,-5,-1,-3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
1097			[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,11,-1,-1,-1,-1,-1,-1],
1098			[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,12,-1,-1,-1],
1099			[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 9,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
1100			[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,14,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
1101			[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,15,-1,-1,-1,-1,-1,-1,-1,-1],
1102			[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,16,-1,-1,-1,-1,-1],
1103			[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 9,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
1104			[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,18,-1,-1,-1],
1105			[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,19,-1,-1,-1,-1,-1,-1,-1,-1],
1106			[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 9,-1,-1,-1,-1,-1,-1,-1,-1],
1107			[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,21,22,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
1108			[ 9, 9,-1,-7,-1,-5,-1,-3,-1,-1,-1,-1,-1,23,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
1109			[ 9, 9,-1,-7,-1,-5,-1,-3,-1,-1,-1,-1,-1,23,22,22,-1,-1,-1,-1,24,-1,-1,-1,-1,-1,-1,-1,-1,24,-1],
1110			[ 9, 9,-1,-7,-1,-5,-1,-3,-1,-1,-1,-1,-1,-1,23,23,-1,-1,-1,-1,24,-1,-1,-1,-1,-1,-1,-1,-1,24,-1],
1111			[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,25,25,-1,26,26,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
1112			[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,26,26,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
1113			[ 9, 9,-1,-7,-1,-5,-1,-3,-1,-1,-1,-1,-1,-1,26,26,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
1114			[27,27,-1,-1,-1,-1,-2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
1115			[28,28,-8,-1,-6,-1,-1,-1, 3,-1,-1,-1,20,-1,21,22,-1,-1,-1,-1,-1,13,-1,17,-1,-1,10,-1,-1,-1,-1],
1116			[29,29,-1,-1,-1,-1,-1,-1, 3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1]
1117		];
1118	}
1119
1120	/**
1121	 * Push a mode onto the stack. Return false if there is overflow.
1122	 *
1123	 * @param int $mode Mode to push onto the stack
1124	 * @return bool Success/failure of stack push
1125	 */
1126	protected function _push($mode) {
1127		$this->_the_top++;
1128		if ($this->_the_top >= self::MAX_DEPTH) {
1129			return false;
1130		}
1131		$this->_the_stack[$this->_the_top] = $mode;
1132		return true;
1133	}
1134
1135	/**
1136	 * Pop the stack, assuring that the current mode matches the expectation.
1137	 * Return false if there is underflow or if the modes mismatch.
1138	 *
1139	 * @param int $mode Mode to pop from the stack
1140	 * @return bool Success/failure of stack pop
1141	 */
1142	protected function _pop($mode) {
1143		if ($this->_the_top < 0 || $this->_the_stack[$this->_the_top] != $mode) {
1144			return false;
1145		}
1146		$this->_the_stack[$this->_the_top] = 0;
1147		$this->_the_top--;
1148		return true;
1149	}
1150}
1151