PHP8 fix
[pear] / Services / JSON.php
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 307804 2011-01-28 00:16:42Z 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  */
61 define('SERVICES_JSON_SLICE',   1);
62
63 /**
64  * Marker constant for Services_JSON::decode(), used to flag stack state
65  */
66 define('SERVICES_JSON_IN_STR',  2);
67
68 /**
69  * Marker constant for Services_JSON::decode(), used to flag stack state
70  */
71 define('SERVICES_JSON_IN_ARR',  3);
72
73 /**
74  * Marker constant for Services_JSON::decode(), used to flag stack state
75  */
76 define('SERVICES_JSON_IN_OBJ',  4);
77
78 /**
79  * Marker constant for Services_JSON::decode(), used to flag stack state
80  */
81 define('SERVICES_JSON_IN_CMT', 5);
82
83 /**
84  * Behavior switch for Services_JSON::decode()
85  */
86 define('SERVICES_JSON_LOOSE_TYPE', 16);
87
88 /**
89  * Behavior switch for Services_JSON::decode()
90  */
91 define('SERVICES_JSON_SUPPRESS_ERRORS', 32);
92
93 /**
94  * Behavior switch for Services_JSON::decode()
95  */
96 define('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  */
119 class 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 __construct($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     // tab and crlf are used by stringfy to produce pretty JSON.
155     var $_tab = '';
156     var $_crlf = '';
157     var $_indent = 0;
158     
159     var $use; // ??
160    /**
161     * convert a string from one UTF-16 char to one UTF-8 char
162     *
163     * Normally should be handled by mb_convert_encoding, but
164     * provides a slower PHP-only method for installations
165     * that lack the multibye string extension.
166     *
167     * @param    string  $utf16  UTF-16 character
168     * @return   string  UTF-8 character
169     * @access   private
170     */
171     function utf162utf8($utf16)
172     {
173         // oh please oh please oh please oh please oh please
174         if($this->_mb_convert_encoding) {
175             return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16');
176         }
177
178         $bytes = (ord($utf16[0]) << 8) | ord($utf16[1]);
179
180         switch(true) {
181             case ((0x7F & $bytes) == $bytes):
182                 // this case should never be reached, because we are in ASCII range
183                 // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
184                 return chr(0x7F & $bytes);
185
186             case (0x07FF & $bytes) == $bytes:
187                 // return a 2-byte UTF-8 character
188                 // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
189                 return chr(0xC0 | (($bytes >> 6) & 0x1F))
190                      . chr(0x80 | ($bytes & 0x3F));
191
192             case (0xFFFF & $bytes) == $bytes:
193                 // return a 3-byte UTF-8 character
194                 // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
195                 return chr(0xE0 | (($bytes >> 12) & 0x0F))
196                      . chr(0x80 | (($bytes >> 6) & 0x3F))
197                      . chr(0x80 | ($bytes & 0x3F));
198         }
199
200         // ignoring UTF-32 for now, sorry
201         return '';
202     }
203
204    /**
205     * convert a string from one UTF-8 char to one UTF-16 char
206     *
207     * Normally should be handled by mb_convert_encoding, but
208     * provides a slower PHP-only method for installations
209     * that lack the multibye string extension.
210     *
211     * @param    string  $utf8   UTF-8 character
212     * @return   string  UTF-16 character
213     * @access   private
214     */
215     function utf82utf16($utf8)
216     {
217         // oh please oh please oh please oh please oh please
218         if($this->_mb_convert_encoding) {
219             return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
220         }
221
222         switch($this->strlen8($utf8)) {
223             case 1:
224                 // this case should never be reached, because we are in ASCII range
225                 // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
226                 return $utf8;
227
228             case 2:
229                 // return a UTF-16 character from a 2-byte UTF-8 char
230                 // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
231                 return chr(0x07 & (ord($utf8[0]) >> 2))
232                      . chr((0xC0 & (ord($utf8[0]) << 6))
233                          | (0x3F & ord($utf8[1])));
234
235             case 3:
236                 // return a UTF-16 character from a 3-byte UTF-8 char
237                 // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
238                 return chr((0xF0 & (ord($utf8[0]) << 4))
239                          | (0x0F & (ord($utf8[1]) >> 2)))
240                      . chr((0xC0 & (ord($utf8[1]) << 6))
241                          | (0x7F & ord($utf8[2])));
242         }
243
244         // ignoring UTF-32 for now, sorry
245         return '';
246     }
247
248     
249    /**
250     * stringfy an arbitrary variable into JSON format (and sends JSON Header)
251     * UNSAFE - does not send HTTP headers (to be compatible with Javsacript Spec)
252     *
253     * @param    mixed   $var    any number, boolean, string, array, or object to be encoded.
254     *                           see argument 1 to Services_JSON() above for array-parsing behavior.
255     *                           if var is a strng, note that encode() always expects it
256     *                           to be in ASCII or UTF-8 format!
257     * @param    mixed   $replacer    NOT SUPPORTED YET.
258     * @param    number|string   $space 
259     *                           an optional parameter that specifies the indentation
260     *                           of nested structures. If it is omitted, the text will
261     *                           be packed without extra whitespace. If it is a number,
262     *                           it will specify the number of spaces to indent at each
263     *                           level. If it is a string (such as '\t' or '&nbsp;'),
264     *                           it contains the characters used to indent at each level.
265     *
266     * @return   mixed   JSON string representation of input var or an error if a problem occurs
267     * @access   public
268     */
269     static function stringify($var, $replacer=false, $space=false)
270     {
271         //header('Content-type: application/json');
272         $s = new Services_JSON(SERVICES_JSON_USE_TO_JSON);
273         
274         $s->_tab = is_numeric($space) ? str_repeat(' ', $space) : $space;
275         $s->_crlf = "\n";
276         $s->_indent = 0;
277         return  $s->encodeUnsafe($var);
278         
279     }
280     /**
281     * encodes an arbitrary variable into JSON format (and sends JSON Header)
282     *
283     * @param    mixed   $var    any number, boolean, string, array, or object to be encoded.
284     *                           see argument 1 to Services_JSON() above for array-parsing behavior.
285     *                           if var is a strng, note that encode() always expects it
286     *                           to be in ASCII or UTF-8 format!
287     *
288     * @return   mixed   JSON string representation of input var or an error if a problem occurs
289     * @access   public
290     */
291     function encode($var)
292     {
293         header('Content-type: text/javascript'); // changed to make debugging easier.
294         return $this->encodeUnsafe($var);
295     }
296     /**
297     * encodes an arbitrary variable into JSON format without JSON Header - warning - may allow XSS!!!!)
298     *
299     * @param    mixed   $var    any number, boolean, string, array, or object to be encoded.
300     *                           see argument 1 to Services_JSON() above for array-parsing behavior.
301     *                           if var is a strng, note that encode() always expects it
302     *                           to be in ASCII or UTF-8 format!
303     *
304     * @return   mixed   JSON string representation of input var or an error if a problem occurs
305     * @access   public
306     */
307     function encodeUnsafe($var)
308     {
309         // see bug #16908 - regarding numeric locale printing
310         $lc = setlocale(LC_NUMERIC, 0);
311         setlocale(LC_NUMERIC, 'C');
312         $ret = $this->_encode($var);
313         setlocale(LC_NUMERIC, $lc);
314         return $ret;
315         
316     }
317     /**
318     * PRIVATE CODE that does the work of encodes an arbitrary variable into JSON format 
319     *
320     * @param    mixed   $var    any number, boolean, string, array, or object to be encoded.
321     *                           see argument 1 to Services_JSON() above for array-parsing behavior.
322     *                           if var is a strng, note that encode() always expects it
323     *                           to be in ASCII or UTF-8 format!
324     *
325     * @return   mixed   JSON string representation of input var or an error if a problem occurs
326     * @access   public
327     */
328     function _encode($var) 
329     {
330         $ind = str_repeat($this->_tab, $this->_indent);
331         $indx = $ind . $this->_tab; 
332         
333         switch (gettype($var)) {
334             case 'boolean':
335                 return $var ? 'true' : 'false';
336
337             case 'NULL':
338                 return 'null';
339
340             case 'integer':
341                 return (int) $var;
342
343             case 'double':
344             case 'float':
345                 return  (float) $var;
346
347             case 'string':
348                 // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT
349                 $ascii = '';
350                 $strlen_var = $this->strlen8($var);
351
352                /*
353                 * Iterate over every character in the string,
354                 * escaping with a slash or encoding to UTF-8 where necessary
355                 */
356                 for ($c = 0; $c < $strlen_var; ++$c) {
357
358                     $ord_var_c = ord($var[$c]);
359
360                     switch (true) {
361                         case $ord_var_c == 0x08:
362                             $ascii .= '\b';
363                             break;
364                         case $ord_var_c == 0x09:
365                             $ascii .= '\t';
366                             break;
367                         case $ord_var_c == 0x0A:
368                             $ascii .= '\n';
369                             break;
370                         case $ord_var_c == 0x0C:
371                             $ascii .= '\f';
372                             break;
373                         case $ord_var_c == 0x0D:
374                             $ascii .= '\r';
375                             break;
376
377                         case $ord_var_c == 0x22:
378                         case $ord_var_c == 0x2F:
379                         case $ord_var_c == 0x5C:
380                             // double quote, slash, slosh
381                             $ascii .= '\\'.$var[$c];
382                             break;
383
384                         case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)):
385                             // characters U-00000000 - U-0000007F (same as ASCII)
386                             $ascii .= $var[$c];
387                             break;
388
389                         case (($ord_var_c & 0xE0) == 0xC0):
390                             // characters U-00000080 - U-000007FF, mask 110XXXXX
391                             // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
392                             if ($c+1 >= $strlen_var) {
393                                 $c += 1;
394                                 $ascii .= '?';
395                                 break;
396                             }
397                             
398                             $char = pack('C*', $ord_var_c, ord($var[$c + 1]));
399                             $c += 1;
400                             $utf16 = $this->utf82utf16($char);
401                             $ascii .= sprintf('\u%04s', bin2hex($utf16));
402                             break;
403
404                         case (($ord_var_c & 0xF0) == 0xE0):
405                             if ($c+2 >= $strlen_var) {
406                                 $c += 2;
407                                 $ascii .= '?';
408                                 break;
409                             }
410                             // characters U-00000800 - U-0000FFFF, mask 1110XXXX
411                             // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
412                             $char = pack('C*', $ord_var_c,
413                                          @ord($var[$c + 1]),
414                                          @ord($var[$c + 2]));
415                             $c += 2;
416                             $utf16 = $this->utf82utf16($char);
417                             $ascii .= sprintf('\u%04s', bin2hex($utf16));
418                             break;
419
420                         case (($ord_var_c & 0xF8) == 0xF0):
421                             if ($c+3 >= $strlen_var) {
422                                 $c += 3;
423                                 $ascii .= '?';
424                                 break;
425                             }
426                             // characters U-00010000 - U-001FFFFF, mask 11110XXX
427                             // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
428                             $char = pack('C*', $ord_var_c,
429                                          ord($var[$c + 1]),
430                                          ord($var[$c + 2]),
431                                          ord($var[$c + 3]));
432                             $c += 3;
433                             $utf16 = $this->utf82utf16($char);
434                             $ascii .= sprintf('\u%04s', bin2hex($utf16));
435                             break;
436
437                         case (($ord_var_c & 0xFC) == 0xF8):
438                             // characters U-00200000 - U-03FFFFFF, mask 111110XX
439                             // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
440                             if ($c+4 >= $strlen_var) {
441                                 $c += 4;
442                                 $ascii .= '?';
443                                 break;
444                             }
445                             $char = pack('C*', $ord_var_c,
446                                          ord($var[$c + 1]),
447                                          ord($var[$c + 2]),
448                                          ord($var[$c + 3]),
449                                          ord($var[$c + 4]));
450                             $c += 4;
451                             $utf16 = $this->utf82utf16($char);
452                             $ascii .= sprintf('\u%04s', bin2hex($utf16));
453                             break;
454
455                         case (($ord_var_c & 0xFE) == 0xFC):
456                         if ($c+5 >= $strlen_var) {
457                                 $c += 5;
458                                 $ascii .= '?';
459                                 break;
460                             }
461                             // characters U-04000000 - U-7FFFFFFF, mask 1111110X
462                             // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
463                             $char = pack('C*', $ord_var_c,
464                                          ord($var[$c + 1]),
465                                          ord($var[$c + 2]),
466                                          ord($var[$c + 3]),
467                                          ord($var[$c + 4]),
468                                          ord($var[$c + 5]));
469                             $c += 5;
470                             $utf16 = $this->utf82utf16($char);
471                             $ascii .= sprintf('\u%04s', bin2hex($utf16));
472                             break;
473                     }
474                 }
475                 return  '"'.$ascii.'"';
476
477             case 'array':
478                /*
479                 * As per JSON spec if any array key is not an integer
480                 * we must treat the the whole array as an object. We
481                 * also try to catch a sparsely populated associative
482                 * array with numeric keys here because some JS engines
483                 * will create an array with empty indexes up to
484                 * max_index which can cause memory issues and because
485                 * the keys, which may be relevant, will be remapped
486                 * otherwise.
487                 *
488                 * As per the ECMA and JSON specification an object may
489                 * have any string as a property. Unfortunately due to
490                 * a hole in the ECMA specification if the key is a
491                 * ECMA reserved word or starts with a digit the
492                 * parameter is only accessible using ECMAScript's
493                 * bracket notation.
494                 */
495
496                 // treat as a JSON object
497                 if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) {
498                     $this->_indent++;
499                     $properties = array_map(array($this, 'name_value'),
500                                             array_keys($var),
501                                             array_values($var));
502                     $this->_indent--;
503                     foreach($properties as $property) {
504                         if(Services_JSON::isError($property)) {
505                             return $property;
506                         }
507                     }
508                     
509                     return "{" . $this->_crlf .  $indx .  
510                         join(",". $this->_crlf . $indx, $properties) . $this->_crlf .
511                         $ind."}";
512                     
513                 }
514
515                 // treat it like a regular array
516                 $this->_indent++;
517                 $elements = array_map(array($this, '_encode'), $var);
518                 $this->_indent--;
519                 
520                 foreach($elements as $element) {
521                     if(Services_JSON::isError($element)) {
522                         return $element;
523                     }
524                 }
525                 
526                 $pad = $this->_tab === '' ? '' : ' ';
527                 
528                 // short array, just show it on one line.
529                 if (strlen(join(',' . $pad, $elements)) < 30) {
530                     return '[' . join(',' . $pad, $elements) . ']';
531                 }
532                 
533                 return "[" . $this->_crlf .
534                     $indx .  join(",". $this->_crlf . $indx, $elements) . $this->_crlf .
535                     $ind . "]";
536
537             case 'object':
538             
539                 // support toJSON methods.
540                 if (($this->use & SERVICES_JSON_USE_TO_JSON) && method_exists($var, 'toJSON')) {
541                     // this may end up allowing unlimited recursion
542                     // so we check the return value to make sure it's not got the same method.
543                     $recode = $var->toJSON();
544                     
545                     if (method_exists($recode, 'toJSON')) {
546                         
547                         return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS)
548                         ? 'null'
549                         : new Services_JSON_Error(get_class($var).
550                             " toJSON returned an object with a toJSON method.");
551                             
552                     }
553                     
554                     return $this->_encode( $recode );
555                 } 
556                 
557                 $vars = get_object_vars($var);
558                 
559                 $this->_indent++;
560                 $properties = array_map(array($this, 'name_value'),
561                                         array_keys($vars),
562                                         array_values($vars));
563                 $this->_indent--;
564                 
565                 foreach($properties as $property) {
566                     if(Services_JSON::isError($property)) {
567                         return $property;
568                     }
569                 }
570                 
571                 return "{" . $this->_crlf .
572                     $indx .  join(",". $this->_crlf . $indx, $properties) . $this->_crlf .
573                     $ind . "}";
574                 
575             default:
576                 return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS)
577                     ? 'null'
578                     : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string");
579         }
580     }
581
582    /**
583     * array-walking function for use in generating JSON-formatted name-value pairs
584     *
585     * @param    string  $name   name of key to use
586     * @param    mixed   $value  reference to an array element to be encoded
587     *
588     * @return   string  JSON-formatted name-value pair, like '"name":value'
589     * @access   private
590     */
591     function name_value($name, $value)
592     {
593         $encoded_value = $this->_encode($value);
594         
595         if(Services_JSON::isError($encoded_value)) {
596             return $encoded_value;
597         }
598         
599         $pad = $this->_tab === '' ? '' : ' ';
600         
601         return $this->_encode(strval($name)) . $pad . ':' . $pad . $encoded_value;
602     }
603
604    /**
605     * reduce a string by removing leading and trailing comments and whitespace
606     *
607     * @param    $str    string      string value to strip of comments and whitespace
608     *
609     * @return   string  string value stripped of comments and whitespace
610     * @access   private
611     */
612     function reduce_string($str)
613     {
614         $str = preg_replace(array(
615
616                 // eliminate single line comments in '// ...' form
617                 '#^\s*//(.+)$#m',
618
619                 // eliminate multi-line comments in '/* ... */' form, at start of string
620                 '#^\s*/\*(.+)\*/#Us',
621
622                 // eliminate multi-line comments in '/* ... */' form, at end of string
623                 '#/\*(.+)\*/\s*$#Us'
624
625             ), '', $str);
626
627         // eliminate extraneous space
628         return trim($str);
629     }
630
631    /**
632     * decodes a JSON string into appropriate variable
633     *
634     * @param    string  $str    JSON-formatted string
635     *
636     * @return   mixed   number, boolean, string, array, or object
637     *                   corresponding to given JSON input string.
638     *                   See argument 1 to Services_JSON() above for object-output behavior.
639     *                   Note that decode() always returns strings
640     *                   in ASCII or UTF-8 format!
641     * @access   public
642     */
643     function decode($str)
644     {
645         $str = $this->reduce_string($str);
646
647         switch (strtolower($str)) {
648             case 'true':
649                 return true;
650
651             case 'false':
652                 return false;
653
654             case 'null':
655                 return null;
656
657             default:
658                 $m = array();
659
660                 if (is_numeric($str)) {
661                     // Lookie-loo, it's a number
662
663                     // This would work on its own, but I'm trying to be
664                     // good about returning integers where appropriate:
665                     // return (float)$str;
666
667                     // Return float or int, as appropriate
668                     return ((float)$str == (integer)$str)
669                         ? (integer)$str
670                         : (float)$str;
671
672                 } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) {
673                     // STRINGS RETURNED IN UTF-8 FORMAT
674                     $delim = $this->substr8($str, 0, 1);
675                     $chrs = $this->substr8($str, 1, -1);
676                     $utf8 = '';
677                     $strlen_chrs = $this->strlen8($chrs);
678
679                     for ($c = 0; $c < $strlen_chrs; ++$c) {
680
681                         $substr_chrs_c_2 = $this->substr8($chrs, $c, 2);
682                         $ord_chrs_c = ord($chrs[$c]);
683
684                         switch (true) {
685                             case $substr_chrs_c_2 == '\b':
686                                 $utf8 .= chr(0x08);
687                                 ++$c;
688                                 break;
689                             case $substr_chrs_c_2 == '\t':
690                                 $utf8 .= chr(0x09);
691                                 ++$c;
692                                 break;
693                             case $substr_chrs_c_2 == '\n':
694                                 $utf8 .= chr(0x0A);
695                                 ++$c;
696                                 break;
697                             case $substr_chrs_c_2 == '\f':
698                                 $utf8 .= chr(0x0C);
699                                 ++$c;
700                                 break;
701                             case $substr_chrs_c_2 == '\r':
702                                 $utf8 .= chr(0x0D);
703                                 ++$c;
704                                 break;
705
706                             case $substr_chrs_c_2 == '\\"':
707                             case $substr_chrs_c_2 == '\\\'':
708                             case $substr_chrs_c_2 == '\\\\':
709                             case $substr_chrs_c_2 == '\\/':
710                                 if (($delim == '"' && $substr_chrs_c_2 != '\\\'') ||
711                                    ($delim == "'" && $substr_chrs_c_2 != '\\"')) {
712                                     $utf8 .= $chrs[++$c];
713                                 }
714                                 break;
715
716                             case preg_match('/\\\u[0-9A-F]{4}/i', $this->substr8($chrs, $c, 6)):
717                                 // single, escaped unicode character
718                                 $utf16 = chr(hexdec($this->substr8($chrs, ($c + 2), 2)))
719                                        . chr(hexdec($this->substr8($chrs, ($c + 4), 2)));
720                                 $utf8 .= $this->utf162utf8($utf16);
721                                 $c += 5;
722                                 break;
723
724                             case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F):
725                                 $utf8 .= $chrs[$c];
726                                 break;
727
728                             case ($ord_chrs_c & 0xE0) == 0xC0:
729                                 // characters U-00000080 - U-000007FF, mask 110XXXXX
730                                 //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
731                                 $utf8 .= $this->substr8($chrs, $c, 2);
732                                 ++$c;
733                                 break;
734
735                             case ($ord_chrs_c & 0xF0) == 0xE0:
736                                 // characters U-00000800 - U-0000FFFF, mask 1110XXXX
737                                 // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
738                                 $utf8 .= $this->substr8($chrs, $c, 3);
739                                 $c += 2;
740                                 break;
741
742                             case ($ord_chrs_c & 0xF8) == 0xF0:
743                                 // characters U-00010000 - U-001FFFFF, mask 11110XXX
744                                 // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
745                                 $utf8 .= $this->substr8($chrs, $c, 4);
746                                 $c += 3;
747                                 break;
748
749                             case ($ord_chrs_c & 0xFC) == 0xF8:
750                                 // characters U-00200000 - U-03FFFFFF, mask 111110XX
751                                 // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
752                                 $utf8 .= $this->substr8($chrs, $c, 5);
753                                 $c += 4;
754                                 break;
755
756                             case ($ord_chrs_c & 0xFE) == 0xFC:
757                                 // characters U-04000000 - U-7FFFFFFF, mask 1111110X
758                                 // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
759                                 $utf8 .= $this->substr8($chrs, $c, 6);
760                                 $c += 5;
761                                 break;
762
763                         }
764
765                     }
766
767                     return $utf8;
768
769                 } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) {
770                     // array, or object notation
771
772                     if ($str[0] == '[') {
773                         $stk = array(SERVICES_JSON_IN_ARR);
774                         $arr = array();
775                     } else {
776                         if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
777                             $stk = array(SERVICES_JSON_IN_OBJ);
778                             $obj = array();
779                         } else {
780                             $stk = array(SERVICES_JSON_IN_OBJ);
781                             $obj = new stdClass();
782                         }
783                     }
784
785                     array_push($stk, array('what'  => SERVICES_JSON_SLICE,
786                                            'where' => 0,
787                                            'delim' => false));
788
789                     $chrs = $this->substr8($str, 1, -1);
790                     $chrs = $this->reduce_string($chrs);
791
792                     if ($chrs == '') {
793                         if (reset($stk) == SERVICES_JSON_IN_ARR) {
794                             return $arr;
795
796                         } else {
797                             return $obj;
798
799                         }
800                     }
801
802                     //print("\nparsing {$chrs}\n");
803
804                     $strlen_chrs = $this->strlen8($chrs);
805
806                     for ($c = 0; $c <= $strlen_chrs; ++$c) {
807
808                         $top = end($stk);
809                         $substr_chrs_c_2 = $this->substr8($chrs, $c, 2);
810
811                         if (($c == $strlen_chrs) || (($chrs[$c] == ',') && ($top['what'] == SERVICES_JSON_SLICE))) {
812                             // found a comma that is not inside a string, array, etc.,
813                             // OR we've reached the end of the character list
814                             $slice = $this->substr8($chrs, $top['where'], ($c - $top['where']));
815                             array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false));
816                             //print("Found split at [$c]: ".$this->substr8($chrs, $top['where'], (1 + $c - $top['where']))."\n");
817
818                             if (reset($stk) == SERVICES_JSON_IN_ARR) {
819                                 // we are in an array, so just push an element onto the stack
820                                 array_push($arr, $this->decode($slice));
821
822                             } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
823                                 // we are in an object, so figure
824                                 // out the property name and set an
825                                 // element in an associative array,
826                                 // for now
827                                 $parts = array();
828                                 
829                                if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:/Uis', $slice, $parts)) {
830                                       // "name":value pair
831                                     $key = $this->decode($parts[1]);
832                                     $val = $this->decode(trim(substr($slice, strlen($parts[0])), ", \t\n\r\0\x0B"));
833                                     if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
834                                         $obj[$key] = $val;
835                                     } else {
836                                         $obj->$key = $val;
837                                     }
838                                 } elseif (preg_match('/^\s*(\w+)\s*:/Uis', $slice, $parts)) {
839                                     // name:value pair, where name is unquoted
840                                     $key = $parts[1];
841                                     $val = $this->decode(trim(substr($slice, strlen($parts[0])), ", \t\n\r\0\x0B"));
842
843                                     if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
844                                         $obj[$key] = $val;
845                                     } else {
846                                         $obj->$key = $val;
847                                     }
848                                 }
849
850                             }
851
852                         } elseif ((($chrs[$c] == '"') || ($chrs[$c] == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) {
853                             // found a quote, and we are not inside a string
854                             array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs[$c]));
855                             //print("Found start of string at [$c]\n");
856
857                         } elseif (($chrs[$c] == $top['delim']) &&
858                                  ($top['what'] == SERVICES_JSON_IN_STR) &&
859                                  (($this->strlen8($this->substr8($chrs, 0, $c)) - $this->strlen8(rtrim($this->substr8($chrs, 0, $c), '\\'))) % 2 != 1)) {
860                             // found a quote, we're in a string, and it's not escaped
861                             // we know that it's not escaped becase there is _not_ an
862                             // odd number of backslashes at the end of the string so far
863                             array_pop($stk);
864                             //print("Found end of string at [$c]: ".$this->substr8($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n");
865
866                         } elseif (($chrs[$c] == '[') &&
867                                  in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
868                             // found a left-bracket, and we are in an array, object, or slice
869                             array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false));
870                             //print("Found start of array at [$c]\n");
871
872                         } elseif (($chrs[$c] == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) {
873                             // found a right-bracket, and we're in an array
874                             array_pop($stk);
875                             //print("Found end of array at [$c]: ".$this->substr8($chrs, $top['where'], (1 + $c - $top['where']))."\n");
876
877                         } elseif (($chrs[$c] == '{') &&
878                                  in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
879                             // found a left-brace, and we are in an array, object, or slice
880                             array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false));
881                             //print("Found start of object at [$c]\n");
882
883                         } elseif (($chrs[$c] == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) {
884                             // found a right-brace, and we're in an object
885                             array_pop($stk);
886                             //print("Found end of object at [$c]: ".$this->substr8($chrs, $top['where'], (1 + $c - $top['where']))."\n");
887
888                         } elseif (($substr_chrs_c_2 == '/*') &&
889                                  in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
890                             // found a comment start, and we are in an array, object, or slice
891                             array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false));
892                             $c++;
893                             //print("Found start of comment at [$c]\n");
894
895                         } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) {
896                             // found a comment end, and we're in one now
897                             array_pop($stk);
898                             $c++;
899
900                             for ($i = $top['where']; $i <= $c; ++$i)
901                                 $chrs = substr_replace($chrs, ' ', $i, 1);
902
903                             //print("Found end of comment at [$c]: ".$this->substr8($chrs, $top['where'], (1 + $c - $top['where']))."\n");
904
905                         }
906
907                     }
908
909                     if (reset($stk) == SERVICES_JSON_IN_ARR) {
910                         return $arr;
911
912                     } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
913                         return $obj;
914
915                     }
916
917                 }
918         }
919     }
920
921     /**
922      * @todo Ultimately, this should just call PEAR::isError()
923      */
924     function isError($data, $code = null)
925     {
926         if (class_exists('pear')) {
927             return PEAR::isError($data, $code);
928         } elseif (is_object($data) && (get_class($data) == 'services_json_error' ||
929                                  is_subclass_of($data, 'services_json_error'))) {
930             return true;
931         }
932
933         return false;
934     }
935     
936     /**
937     * Calculates length of string in bytes
938     * @param string 
939     * @return integer length
940     */
941     function strlen8( $str ) 
942     {
943         if ( $this->_mb_strlen ) {
944             return mb_strlen( $str, "8bit" );
945         }
946         return strlen( $str );
947     }
948     
949     /**
950     * Returns part of a string, interpreting $start and $length as number of bytes.
951     * @param string 
952     * @param integer start 
953     * @param integer length 
954     * @return integer length
955     */
956     function substr8( $string, $start, $length=false ) 
957     {
958         if ( $length === false ) {
959             $length = $this->strlen8( $string ) - $start;
960         }
961         if ( $this->_mb_substr ) {
962             return mb_substr( $string, $start, $length, "8bit" );
963         }
964         return substr( $string, $start, $length );
965     }
966
967 }
968
969 if (class_exists('PEAR_Error')) {
970
971     class Services_JSON_Error extends PEAR_Error
972     {
973         function __construct($message = 'unknown error', $code = null,
974                                      $mode = null, $options = null, $userinfo = null)
975         {
976             parent::__construct($message, $code, $mode, $options, $userinfo);
977         }
978     }
979
980 } else {
981
982     /**
983      * @todo Ultimately, this class shall be descended from PEAR_Error
984      */
985     class Services_JSON_Error
986     {
987         function __construct($message = 'unknown error', $code = null,
988                                      $mode = null, $options = null, $userinfo = null)
989         {
990             return;
991         }
992     }
993     
994 }