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