[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/ -> JSON.php (source)

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