1<?php 2/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ 3/** 4 * Converts to and from JSON format. 5 * 6 * JSON (JavaScript Object Notation) is a lightweight data-interchange 7 * format. It is easy for humans to read and write. It is easy for machines 8 * to parse and generate. It is based on a subset of the JavaScript 9 * Programming Language, Standard ECMA-262 3rd Edition - December 1999. 10 * This feature can also be found in Python. JSON is a text format that is 11 * completely language independent but uses conventions that are familiar 12 * to programmers of the C-family of languages, including C, C++, C#, Java, 13 * JavaScript, Perl, TCL, and many others. These properties make JSON an 14 * ideal data-interchange language. 15 * 16 * This package provides a simple encoder and decoder for JSON notation. It 17 * is intended for use with client-side Javascript applications that make 18 * use of HTTPRequest to perform server communication functions - data can 19 * be encoded into JSON notation for use in a client-side javascript, or 20 * decoded from incoming Javascript requests. JSON format is native to 21 * Javascript, and can be directly eval()'ed with no further parsing 22 * overhead 23 * 24 * All strings should be in ASCII or UTF-8 format! 25 * 26 * LICENSE: Redistribution and use in source and binary forms, with or 27 * without modification, are permitted provided that the following 28 * conditions are met: Redistributions of source code must retain the 29 * above copyright notice, this list of conditions and the following 30 * disclaimer. Redistributions in binary form must reproduce the above 31 * copyright notice, this list of conditions and the following disclaimer 32 * in the documentation and/or other materials provided with the 33 * distribution. 34 * 35 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED 36 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 37 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 38 * NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 39 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 40 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS 41 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 42 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 43 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 44 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 45 * DAMAGE. 46 * 47 * @category 48 * @package Services_JSON 49 * @author Michal Migurski <mike-json@teczno.com> 50 * @author Matt Knapp <mdknapp[at]gmail[dot]com> 51 * @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com> 52 * @copyright 2005 Michal Migurski 53 * @version CVS: $Id: JSON.php 305040 2010-11-02 23:19:03Z alan_k $ 54 * @license http://www.opensource.org/licenses/bsd-license.php 55 * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198 56 */ 57 58/** 59 * Marker constant for Services_JSON::decode(), used to flag stack state 60 */ 61define('SERVICES_JSON_SLICE', 1); 62 63/** 64 * Marker constant for Services_JSON::decode(), used to flag stack state 65 */ 66define('SERVICES_JSON_IN_STR', 2); 67 68/** 69 * Marker constant for Services_JSON::decode(), used to flag stack state 70 */ 71define('SERVICES_JSON_IN_ARR', 3); 72 73/** 74 * Marker constant for Services_JSON::decode(), used to flag stack state 75 */ 76define('SERVICES_JSON_IN_OBJ', 4); 77 78/** 79 * Marker constant for Services_JSON::decode(), used to flag stack state 80 */ 81define('SERVICES_JSON_IN_CMT', 5); 82 83/** 84 * Behavior switch for Services_JSON::decode() 85 */ 86define('SERVICES_JSON_LOOSE_TYPE', 16); 87 88/** 89 * Behavior switch for Services_JSON::decode() 90 */ 91define('SERVICES_JSON_SUPPRESS_ERRORS', 32); 92 93/** 94 * Behavior switch for Services_JSON::decode() 95 */ 96define('SERVICES_JSON_USE_TO_JSON', 64); 97 98/** 99 * Converts to and from JSON format. 100 * 101 * Brief example of use: 102 * 103 * <code> 104 * // create a new instance of Services_JSON 105 * $json = new Services_JSON(); 106 * 107 * // convert a complexe value to JSON notation, and send it to the browser 108 * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4))); 109 * $output = $json->encode($value); 110 * 111 * print($output); 112 * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]] 113 * 114 * // accept incoming POST data, assumed to be in JSON notation 115 * $input = file_get_contents('php://input', 1000000); 116 * $value = $json->decode($input); 117 * </code> 118 */ 119class Services_JSON 120{ 121 /** 122 * constructs a new JSON instance 123 * 124 * @param int $use object behavior flags; combine with boolean-OR 125 * 126 * possible values: 127 * - SERVICES_JSON_LOOSE_TYPE: loose typing. 128 * "{...}" syntax creates associative arrays 129 * instead of objects in decode(). 130 * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression. 131 * Values which can't be encoded (e.g. resources) 132 * appear as NULL instead of throwing errors. 133 * By default, a deeply-nested resource will 134 * bubble up with an error, so all return values 135 * from encode() should be checked with isError() 136 * - SERVICES_JSON_USE_TO_JSON: call toJSON when serializing objects 137 * It serializes the return value from the toJSON call rather 138 * than the object it'self, toJSON can return associative arrays, 139 * strings or numbers, if you return an object, make sure it does 140 * not have a toJSON method, otherwise an error will occur. 141 */ 142 function Services_JSON($use = 0) 143 { 144 $this->use = $use; 145 $this->_mb_strlen = function_exists('mb_strlen'); 146 $this->_mb_convert_encoding = function_exists('mb_convert_encoding'); 147 $this->_mb_substr = function_exists('mb_substr'); 148 } 149 // private - cache the mbstring lookup results.. 150 var $_mb_strlen = false; 151 var $_mb_substr = false; 152 var $_mb_convert_encoding = false; 153 154 /** 155 * convert a string from one UTF-16 char to one UTF-8 char 156 * 157 * Normally should be handled by mb_convert_encoding, but 158 * provides a slower PHP-only method for installations 159 * that lack the multibye string extension. 160 * 161 * @param string $utf16 UTF-16 character 162 * @return string UTF-8 character 163 * @access private 164 */ 165 function utf162utf8($utf16) 166 { 167 // oh please oh please oh please oh please oh please 168 if($this->_mb_convert_encoding) { 169 return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16'); 170 } 171 172 $bytes = (ord($utf16{0}) << 8) | ord($utf16{1}); 173 174 switch(true) { 175 case ((0x7F & $bytes) == $bytes): 176 // this case should never be reached, because we are in ASCII range 177 // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 178 return chr(0x7F & $bytes); 179 180 case (0x07FF & $bytes) == $bytes: 181 // return a 2-byte UTF-8 character 182 // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 183 return chr(0xC0 | (($bytes >> 6) & 0x1F)) 184 . chr(0x80 | ($bytes & 0x3F)); 185 186 case (0xFFFF & $bytes) == $bytes: 187 // return a 3-byte UTF-8 character 188 // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 189 return chr(0xE0 | (($bytes >> 12) & 0x0F)) 190 . chr(0x80 | (($bytes >> 6) & 0x3F)) 191 . chr(0x80 | ($bytes & 0x3F)); 192 } 193 194 // ignoring UTF-32 for now, sorry 195 return ''; 196 } 197 198 /** 199 * convert a string from one UTF-8 char to one UTF-16 char 200 * 201 * Normally should be handled by mb_convert_encoding, but 202 * provides a slower PHP-only method for installations 203 * that lack the multibye string extension. 204 * 205 * @param string $utf8 UTF-8 character 206 * @return string UTF-16 character 207 * @access private 208 */ 209 function utf82utf16($utf8) 210 { 211 // oh please oh please oh please oh please oh please 212 if($this->_mb_convert_encoding) { 213 return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8'); 214 } 215 216 switch($this->strlen8($utf8)) { 217 case 1: 218 // this case should never be reached, because we are in ASCII range 219 // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 220 return $utf8; 221 222 case 2: 223 // return a UTF-16 character from a 2-byte UTF-8 char 224 // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 225 return chr(0x07 & (ord($utf8{0}) >> 2)) 226 . chr((0xC0 & (ord($utf8{0}) << 6)) 227 | (0x3F & ord($utf8{1}))); 228 229 case 3: 230 // return a UTF-16 character from a 3-byte UTF-8 char 231 // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 232 return chr((0xF0 & (ord($utf8{0}) << 4)) 233 | (0x0F & (ord($utf8{1}) >> 2))) 234 . chr((0xC0 & (ord($utf8{1}) << 6)) 235 | (0x7F & ord($utf8{2}))); 236 } 237 238 // ignoring UTF-32 for now, sorry 239 return ''; 240 } 241 242 /** 243 * encodes an arbitrary variable into JSON format (and sends JSON Header) 244 * 245 * @param mixed $var any number, boolean, string, array, or object to be encoded. 246 * see argument 1 to Services_JSON() above for array-parsing behavior. 247 * if var is a strng, note that encode() always expects it 248 * to be in ASCII or UTF-8 format! 249 * 250 * @return mixed JSON string representation of input var or an error if a problem occurs 251 * @access public 252 */ 253 function encode($var) 254 { 255 header('Content-type: application/json'); 256 return $this->encodeUnsafe($var); 257 } 258 /** 259 * encodes an arbitrary variable into JSON format without JSON Header - warning - may allow XSS!!!!) 260 * 261 * @param mixed $var any number, boolean, string, array, or object to be encoded. 262 * see argument 1 to Services_JSON() above for array-parsing behavior. 263 * if var is a strng, note that encode() always expects it 264 * to be in ASCII or UTF-8 format! 265 * 266 * @return mixed JSON string representation of input var or an error if a problem occurs 267 * @access public 268 */ 269 function encodeUnsafe($var) 270 { 271 // see bug #16908 - regarding numeric locale printing 272 $lc = setlocale(LC_NUMERIC, 0); 273 setlocale(LC_NUMERIC, 'C'); 274 $ret = $this->_encode($var); 275 setlocale(LC_NUMERIC, $lc); 276 return $ret; 277 278 } 279 /** 280 * PRIVATE CODE that does the work of encodes an arbitrary variable into JSON format 281 * 282 * @param mixed $var any number, boolean, string, array, or object to be encoded. 283 * see argument 1 to Services_JSON() above for array-parsing behavior. 284 * if var is a strng, note that encode() always expects it 285 * to be in ASCII or UTF-8 format! 286 * 287 * @return mixed JSON string representation of input var or an error if a problem occurs 288 * @access public 289 */ 290 function _encode($var) 291 { 292 293 switch (gettype($var)) { 294 case 'boolean': 295 return $var ? 'true' : 'false'; 296 297 case 'NULL': 298 return 'null'; 299 300 case 'integer': 301 return (int) $var; 302 303 case 'double': 304 case 'float': 305 return (float) $var; 306 307 case 'string': 308 // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT 309 $ascii = ''; 310 $strlen_var = $this->strlen8($var); 311 312 /* 313 * Iterate over every character in the string, 314 * escaping with a slash or encoding to UTF-8 where necessary 315 */ 316 for ($c = 0; $c < $strlen_var; ++$c) { 317 318 $ord_var_c = ord($var{$c}); 319 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 337 case $ord_var_c == 0x22: 338 case $ord_var_c == 0x2F: 339 case $ord_var_c == 0x5C: 340 // double quote, slash, slosh 341 $ascii .= '\\'.$var{$c}; 342 break; 343 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 349 case (($ord_var_c & 0xE0) == 0xC0): 350 // characters U-00000080 - U-000007FF, mask 110XXXXX 351 // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 352 if ($c+1 >= $strlen_var) { 353 $c += 1; 354 $ascii .= '?'; 355 break; 356 } 357 358 $char = pack('C*', $ord_var_c, ord($var{$c + 1})); 359 $c += 1; 360 $utf16 = $this->utf82utf16($char); 361 $ascii .= sprintf('\u%04s', bin2hex($utf16)); 362 break; 363 364 case (($ord_var_c & 0xF0) == 0xE0): 365 if ($c+2 >= $strlen_var) { 366 $c += 2; 367 $ascii .= '?'; 368 break; 369 } 370 // characters U-00000800 - U-0000FFFF, mask 1110XXXX 371 // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 372 $char = pack('C*', $ord_var_c, 373 @ord($var{$c + 1}), 374 @ord($var{$c + 2})); 375 $c += 2; 376 $utf16 = $this->utf82utf16($char); 377 $ascii .= sprintf('\u%04s', bin2hex($utf16)); 378 break; 379 380 case (($ord_var_c & 0xF8) == 0xF0): 381 if ($c+3 >= $strlen_var) { 382 $c += 3; 383 $ascii .= '?'; 384 break; 385 } 386 // characters U-00010000 - U-001FFFFF, mask 11110XXX 387 // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 388 $char = pack('C*', $ord_var_c, 389 ord($var{$c + 1}), 390 ord($var{$c + 2}), 391 ord($var{$c + 3})); 392 $c += 3; 393 $utf16 = $this->utf82utf16($char); 394 $ascii .= sprintf('\u%04s', bin2hex($utf16)); 395 break; 396 397 case (($ord_var_c & 0xFC) == 0xF8): 398 // characters U-00200000 - U-03FFFFFF, mask 111110XX 399 // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 400 if ($c+4 >= $strlen_var) { 401 $c += 4; 402 $ascii .= '?'; 403 break; 404 } 405 $char = pack('C*', $ord_var_c, 406 ord($var{$c + 1}), 407 ord($var{$c + 2}), 408 ord($var{$c + 3}), 409 ord($var{$c + 4})); 410 $c += 4; 411 $utf16 = $this->utf82utf16($char); 412 $ascii .= sprintf('\u%04s', bin2hex($utf16)); 413 break; 414 415 case (($ord_var_c & 0xFE) == 0xFC): 416 if ($c+5 >= $strlen_var) { 417 $c += 5; 418 $ascii .= '?'; 419 break; 420 } 421 // characters U-04000000 - U-7FFFFFFF, mask 1111110X 422 // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 423 $char = pack('C*', $ord_var_c, 424 ord($var{$c + 1}), 425 ord($var{$c + 2}), 426 ord($var{$c + 3}), 427 ord($var{$c + 4}), 428 ord($var{$c + 5})); 429 $c += 5; 430 $utf16 = $this->utf82utf16($char); 431 $ascii .= sprintf('\u%04s', bin2hex($utf16)); 432 break; 433 } 434 } 435 return '"'.$ascii.'"'; 436 437 case 'array': 438 /* 439 * As per JSON spec if any array key is not an integer 440 * we must treat the the whole array as an object. We 441 * also try to catch a sparsely populated associative 442 * array with numeric keys here because some JS engines 443 * will create an array with empty indexes up to 444 * max_index which can cause memory issues and because 445 * the keys, which may be relevant, will be remapped 446 * otherwise. 447 * 448 * As per the ECMA and JSON specification an object may 449 * have any string as a property. Unfortunately due to 450 * a hole in the ECMA specification if the key is a 451 * ECMA reserved word or starts with a digit the 452 * parameter is only accessible using ECMAScript's 453 * bracket notation. 454 */ 455 456 // treat as a JSON object 457 if (is_array($var) && count($var) && (array_keys($var) !== range(0, count($var) - 1))) { 458 $properties = array_map(array($this, 'name_value'), 459 array_keys($var), 460 array_values($var)); 461 462 foreach($properties as $property) { 463 if(Services_JSON::isError($property)) { 464 return $property; 465 } 466 } 467 468 return '{' . join(',', $properties) . '}'; 469 } 470 471 // treat it like a regular array 472 $elements = array_map(array($this, '_encode'), $var); 473 474 foreach($elements as $element) { 475 if(Services_JSON::isError($element)) { 476 return $element; 477 } 478 } 479 480 return '[' . join(',', $elements) . ']'; 481 482 case 'object': 483 484 // support toJSON methods. 485 if (($this->use & SERVICES_JSON_USE_TO_JSON) && method_exists($var, 'toJSON')) { 486 // this may end up allowing unlimited recursion 487 // so we check the return value to make sure it's not got the same method. 488 $recode = $var->toJSON(); 489 490 if (method_exists($recode, 'toJSON')) { 491 492 return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS) 493 ? 'null' 494 : new Services_JSON_Error(class_name($var). 495 " toJSON returned an object with a toJSON method."); 496 497 } 498 499 return $this->_encode( $recode ); 500 } 501 502 $vars = get_object_vars($var); 503 504 $properties = array_map(array($this, 'name_value'), 505 array_keys($vars), 506 array_values($vars)); 507 508 foreach($properties as $property) { 509 if(Services_JSON::isError($property)) { 510 return $property; 511 } 512 } 513 514 return '{' . join(',', $properties) . '}'; 515 516 default: 517 return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS) 518 ? 'null' 519 : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string"); 520 } 521 } 522 523 /** 524 * array-walking function for use in generating JSON-formatted name-value pairs 525 * 526 * @param string $name name of key to use 527 * @param mixed $value reference to an array element to be encoded 528 * 529 * @return string JSON-formatted name-value pair, like '"name":value' 530 * @access private 531 */ 532 function name_value($name, $value) 533 { 534 $encoded_value = $this->_encode($value); 535 536 if(Services_JSON::isError($encoded_value)) { 537 return $encoded_value; 538 } 539 540 return $this->_encode(strval($name)) . ':' . $encoded_value; 541 } 542 543 /** 544 * reduce a string by removing leading and trailing comments and whitespace 545 * 546 * @param $str string string value to strip of comments and whitespace 547 * 548 * @return string string value stripped of comments and whitespace 549 * @access private 550 */ 551 function reduce_string($str) 552 { 553 $str = preg_replace(array( 554 555 // eliminate single line comments in '// ...' form 556 '#^\s*//(.+)$#m', 557 558 // eliminate multi-line comments in '/* ... */' form, at start of string 559 '#^\s*/\*(.+)\*/#Us', 560 561 // eliminate multi-line comments in '/* ... */' form, at end of string 562 '#/\*(.+)\*/\s*$#Us' 563 564 ), '', $str); 565 566 // eliminate extraneous space 567 return trim($str); 568 } 569 570 /** 571 * decodes a JSON string into appropriate variable 572 * 573 * @param string $str JSON-formatted string 574 * 575 * @return mixed number, boolean, string, array, or object 576 * corresponding to given JSON input string. 577 * See argument 1 to Services_JSON() above for object-output behavior. 578 * Note that decode() always returns strings 579 * in ASCII or UTF-8 format! 580 * @access public 581 */ 582 function decode($str) 583 { 584 $str = $this->reduce_string($str); 585 586 switch (strtolower($str)) { 587 case 'true': 588 return true; 589 590 case 'false': 591 return false; 592 593 case 'null': 594 return null; 595 596 default: 597 $m = array(); 598 599 if (is_numeric($str)) { 600 // Lookie-loo, it's a number 601 602 // This would work on its own, but I'm trying to be 603 // good about returning integers where appropriate: 604 // return (float)$str; 605 606 // Return float or int, as appropriate 607 return ((float)$str == (integer)$str) 608 ? (integer)$str 609 : (float)$str; 610 611 } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) { 612 // STRINGS RETURNED IN UTF-8 FORMAT 613 $delim = $this->substr8($str, 0, 1); 614 $chrs = $this->substr8($str, 1, -1); 615 $utf8 = ''; 616 $strlen_chrs = $this->strlen8($chrs); 617 618 for ($c = 0; $c < $strlen_chrs; ++$c) { 619 620 $substr_chrs_c_2 = $this->substr8($chrs, $c, 2); 621 $ord_chrs_c = ord($chrs{$c}); 622 623 switch (true) { 624 case $substr_chrs_c_2 == '\b': 625 $utf8 .= chr(0x08); 626 ++$c; 627 break; 628 case $substr_chrs_c_2 == '\t': 629 $utf8 .= chr(0x09); 630 ++$c; 631 break; 632 case $substr_chrs_c_2 == '\n': 633 $utf8 .= chr(0x0A); 634 ++$c; 635 break; 636 case $substr_chrs_c_2 == '\f': 637 $utf8 .= chr(0x0C); 638 ++$c; 639 break; 640 case $substr_chrs_c_2 == '\r': 641 $utf8 .= chr(0x0D); 642 ++$c; 643 break; 644 645 case $substr_chrs_c_2 == '\\"': 646 case $substr_chrs_c_2 == '\\\'': 647 case $substr_chrs_c_2 == '\\\\': 648 case $substr_chrs_c_2 == '\\/': 649 if (($delim == '"' && $substr_chrs_c_2 != '\\\'') || 650 ($delim == "'" && $substr_chrs_c_2 != '\\"')) { 651 $utf8 .= $chrs{++$c}; 652 } 653 break; 654 655 case preg_match('/\\\u[0-9A-F]{4}/i', $this->substr8($chrs, $c, 6)): 656 // single, escaped unicode character 657 $utf16 = chr(hexdec($this->substr8($chrs, ($c + 2), 2))) 658 . chr(hexdec($this->substr8($chrs, ($c + 4), 2))); 659 $utf8 .= $this->utf162utf8($utf16); 660 $c += 5; 661 break; 662 663 case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F): 664 $utf8 .= $chrs{$c}; 665 break; 666 667 case ($ord_chrs_c & 0xE0) == 0xC0: 668 // characters U-00000080 - U-000007FF, mask 110XXXXX 669 //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 670 $utf8 .= $this->substr8($chrs, $c, 2); 671 ++$c; 672 break; 673 674 case ($ord_chrs_c & 0xF0) == 0xE0: 675 // characters U-00000800 - U-0000FFFF, mask 1110XXXX 676 // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 677 $utf8 .= $this->substr8($chrs, $c, 3); 678 $c += 2; 679 break; 680 681 case ($ord_chrs_c & 0xF8) == 0xF0: 682 // characters U-00010000 - U-001FFFFF, mask 11110XXX 683 // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 684 $utf8 .= $this->substr8($chrs, $c, 4); 685 $c += 3; 686 break; 687 688 case ($ord_chrs_c & 0xFC) == 0xF8: 689 // characters U-00200000 - U-03FFFFFF, mask 111110XX 690 // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 691 $utf8 .= $this->substr8($chrs, $c, 5); 692 $c += 4; 693 break; 694 695 case ($ord_chrs_c & 0xFE) == 0xFC: 696 // characters U-04000000 - U-7FFFFFFF, mask 1111110X 697 // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 698 $utf8 .= $this->substr8($chrs, $c, 6); 699 $c += 5; 700 break; 701 702 } 703 704 } 705 706 return $utf8; 707 708 } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) { 709 // array, or object notation 710 711 if ($str{0} == '[') { 712 $stk = array(SERVICES_JSON_IN_ARR); 713 $arr = array(); 714 } else { 715 if ($this->use & SERVICES_JSON_LOOSE_TYPE) { 716 $stk = array(SERVICES_JSON_IN_OBJ); 717 $obj = array(); 718 } else { 719 $stk = array(SERVICES_JSON_IN_OBJ); 720 $obj = new stdClass(); 721 } 722 } 723 724 array_push($stk, array('what' => SERVICES_JSON_SLICE, 725 'where' => 0, 726 'delim' => false)); 727 728 $chrs = $this->substr8($str, 1, -1); 729 $chrs = $this->reduce_string($chrs); 730 731 if ($chrs == '') { 732 if (reset($stk) == SERVICES_JSON_IN_ARR) { 733 return $arr; 734 735 } else { 736 return $obj; 737 738 } 739 } 740 741 //print("\nparsing {$chrs}\n"); 742 743 $strlen_chrs = $this->strlen8($chrs); 744 745 for ($c = 0; $c <= $strlen_chrs; ++$c) { 746 747 $top = end($stk); 748 $substr_chrs_c_2 = $this->substr8($chrs, $c, 2); 749 750 if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) { 751 // found a comma that is not inside a string, array, etc., 752 // OR we've reached the end of the character list 753 $slice = $this->substr8($chrs, $top['where'], ($c - $top['where'])); 754 array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false)); 755 //print("Found split at {$c}: ".$this->substr8($chrs, $top['where'], (1 + $c - $top['where']))."\n"); 756 757 if (reset($stk) == SERVICES_JSON_IN_ARR) { 758 // we are in an array, so just push an element onto the stack 759 array_push($arr, $this->decode($slice)); 760 761 } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { 762 // we are in an object, so figure 763 // out the property name and set an 764 // element in an associative array, 765 // for now 766 $parts = array(); 767 768 if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:/Uis', $slice, $parts)) { 769 // "name":value pair 770 $key = $this->decode($parts[1]); 771 $val = $this->decode(trim(substr($slice, strlen($parts[0])), ", \t\n\r\0\x0B")); 772 if ($this->use & SERVICES_JSON_LOOSE_TYPE) { 773 $obj[$key] = $val; 774 } else { 775 $obj->$key = $val; 776 } 777 } elseif (preg_match('/^\s*(\w+)\s*:/Uis', $slice, $parts)) { 778 // name:value pair, where name is unquoted 779 $key = $parts[1]; 780 $val = $this->decode(trim(substr($slice, strlen($parts[0])), ", \t\n\r\0\x0B")); 781 782 if ($this->use & SERVICES_JSON_LOOSE_TYPE) { 783 $obj[$key] = $val; 784 } else { 785 $obj->$key = $val; 786 } 787 } 788 789 } 790 791 } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) { 792 // found a quote, and we are not inside a string 793 array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c})); 794 //print("Found start of string at {$c}\n"); 795 796 } elseif (($chrs{$c} == $top['delim']) && 797 ($top['what'] == SERVICES_JSON_IN_STR) && 798 (($this->strlen8($this->substr8($chrs, 0, $c)) - $this->strlen8(rtrim($this->substr8($chrs, 0, $c), '\\'))) % 2 != 1)) { 799 // found a quote, we're in a string, and it's not escaped 800 // we know that it's not escaped becase there is _not_ an 801 // odd number of backslashes at the end of the string so far 802 array_pop($stk); 803 //print("Found end of string at {$c}: ".$this->substr8($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n"); 804 805 } elseif (($chrs{$c} == '[') && 806 in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { 807 // found a left-bracket, and we are in an array, object, or slice 808 array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false)); 809 //print("Found start of array at {$c}\n"); 810 811 } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) { 812 // found a right-bracket, and we're in an array 813 array_pop($stk); 814 //print("Found end of array at {$c}: ".$this->substr8($chrs, $top['where'], (1 + $c - $top['where']))."\n"); 815 816 } elseif (($chrs{$c} == '{') && 817 in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { 818 // found a left-brace, and we are in an array, object, or slice 819 array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false)); 820 //print("Found start of object at {$c}\n"); 821 822 } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) { 823 // found a right-brace, and we're in an object 824 array_pop($stk); 825 //print("Found end of object at {$c}: ".$this->substr8($chrs, $top['where'], (1 + $c - $top['where']))."\n"); 826 827 } elseif (($substr_chrs_c_2 == '/*') && 828 in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { 829 // found a comment start, and we are in an array, object, or slice 830 array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false)); 831 $c++; 832 //print("Found start of comment at {$c}\n"); 833 834 } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) { 835 // found a comment end, and we're in one now 836 array_pop($stk); 837 $c++; 838 839 for ($i = $top['where']; $i <= $c; ++$i) 840 $chrs = substr_replace($chrs, ' ', $i, 1); 841 842 //print("Found end of comment at {$c}: ".$this->substr8($chrs, $top['where'], (1 + $c - $top['where']))."\n"); 843 844 } 845 846 } 847 848 if (reset($stk) == SERVICES_JSON_IN_ARR) { 849 return $arr; 850 851 } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { 852 return $obj; 853 854 } 855 856 } 857 } 858 } 859 860 /** 861 * @todo Ultimately, this should just call PEAR::isError() 862 */ 863 function isError($data, $code = null) 864 { 865 if (class_exists('pear')) { 866 return PEAR::isError($data, $code); 867 } elseif (is_object($data) && (get_class($data) == 'services_json_error' || 868 is_subclass_of($data, 'services_json_error'))) { 869 return true; 870 } 871 872 return false; 873 } 874 875 /** 876 * Calculates length of string in bytes 877 * @param string 878 * @return integer length 879 */ 880 function strlen8( $str ) 881 { 882 if ( $this->_mb_strlen ) { 883 return mb_strlen( $str, "8bit" ); 884 } 885 return strlen( $str ); 886 } 887 888 /** 889 * Returns part of a string, interpreting $start and $length as number of bytes. 890 * @param string 891 * @param integer start 892 * @param integer length 893 * @return integer length 894 */ 895 function substr8( $string, $start, $length=false ) 896 { 897 if ( $length === false ) { 898 $length = $this->strlen8( $string ) - $start; 899 } 900 if ( $this->_mb_substr ) { 901 return mb_substr( $string, $start, $length, "8bit" ); 902 } 903 return substr( $string, $start, $length ); 904 } 905 906} 907 908if (class_exists('PEAR_Error')) { 909 910 class Services_JSON_Error extends PEAR_Error 911 { 912 function Services_JSON_Error($message = 'unknown error', $code = null, 913 $mode = null, $options = null, $userinfo = null) 914 { 915 parent::PEAR_Error($message, $code, $mode, $options, $userinfo); 916 } 917 } 918 919} else { 920 921 /** 922 * @todo Ultimately, this class shall be descended from PEAR_Error 923 */ 924 class Services_JSON_Error 925 { 926 function Services_JSON_Error($message = 'unknown error', $code = null, 927 $mode = null, $options = null, $userinfo = null) 928 { 929 930 } 931 } 932 933} 934