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