Project

General

Profile

Download (28.7 KB) Statistics
| Branch: | Tag: | Revision:
1
<?php
2
/**
3
 * @file
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,v 1.30 2006/03/08 16:10:20 migurski Exp $
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
 * Converts to and from JSON format.
95
 *
96
 * Brief example of use:
97
 *
98
 * <code>
99
 * // create a new instance of Services_JSON
100
 * $json = new Services_JSON();
101
 *
102
 * // convert a complexe value to JSON notation, and send it to the browser
103
 * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4)));
104
 * $output = $json->encode($value);
105
 *
106
 * print($output);
107
 * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]]
108
 *
109
 * // accept incoming POST data, assumed to be in JSON notation
110
 * $input = file_get_contents('php://input', 1000000);
111
 * $value = $json->decode($input);
112
 * <endcode>
113
 */
114
class Services_JSON {
115
  /**
116
   * Constructs a new JSON instance.
117
   *
118
   * @param int $use
119
   *   object behavior flags; combine with boolean-OR.
120
   *   possible values:
121
   *   - SERVICES_JSON_LOOSE_TYPE:  loose typing.
122
   *     "{...}" syntax creates associative arrays
123
   *     instead of objects in decode().
124
   *   - SERVICES_JSON_SUPPRESS_ERRORS: error suppression.
125
   *     Values which can't be encoded (e.g. resources)
126
   *     appear as NULL instead of throwing errors.
127
   *     By default, a deeply-nested resource will bubble up with an error,
128
   *     so all return values from encode() should be checked with isError().
129
   */
130
  function __construct($use = 0) {
131
    $this->use = $use;
132
  }
133

    
134
  /**
135
   * Convert a string from one UTF-16 char to one UTF-8 char.
136
   *
137
   * Normally should be handled by mb_convert_encoding, but
138
   * provides a slower PHP-only method for installations
139
   * that lack the multibye string extension.
140
   *
141
   * @access private
142
   *
143
   * @param string $utf16
144
   *   UTF-16 character.
145
   *
146
   * @return string
147
   *   UTF-8 character.
148
   */
149
  function utf162utf8($utf16) {
150
    // Oh please oh please oh please oh please oh please.
151
    if (function_exists('mb_convert_encoding')) {
152
      return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16');
153
    }
154

    
155
    $bytes = (ord($utf16{0}) << 8) | ord($utf16{1});
156

    
157
    switch (TRUE) {
158
      case ((0x7F & $bytes) == $bytes):
159
        // This case should never be reached, because we are in ASCII range.
160
        // @see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
161
        return chr(0x7F & $bytes);
162

    
163
      case (0x07FF & $bytes) == $bytes:
164
        // Return a 2-byte UTF-8 character.
165
        // @see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
166
        return chr(0xC0 | (($bytes >> 6) & 0x1F))
167
                     . chr(0x80 | ($bytes & 0x3F));
168

    
169
      case (0xFFFF & $bytes) == $bytes:
170
        // Return a 3-byte UTF-8 character.
171
        // @see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
172
        return chr(0xE0 | (($bytes >> 12) & 0x0F))
173
                     . chr(0x80 | (($bytes >> 6) & 0x3F))
174
                     . chr(0x80 | ($bytes & 0x3F));
175
    }
176

    
177
    // Ignoring UTF-32 for now, sorry.
178
    return '';
179
  }
180

    
181
  /**
182
   * convert a string from one UTF-8 char to one UTF-16 char.
183
   *
184
   * Normally should be handled by mb_convert_encoding, but
185
   * provides a slower PHP-only method for installations
186
   * that lack the multibye string extension.
187
   *
188
   * @access private
189
   *
190
   * @param string $utf8
191
   *   UTF-8 character.
192
   *
193
   * @return string
194
   *   UTF-16 character.
195
   */
196
  function utf82utf16($utf8) {
197
    // Oh please oh please oh please oh please oh please.
198
    if (function_exists('mb_convert_encoding')) {
199
      return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8');
200
    }
201

    
202
    switch (strlen($utf8)) {
203
      case 1:
204
        // This case should never be reached, because we are in ASCII range.
205
        // @see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
206
        return $utf8;
207

    
208
      case 2:
209
        // Return a UTF-16 character from a 2-byte UTF-8 char.
210
        // @see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
211
        return chr(0x07 & (ord($utf8{0}) >> 2))
212
                     . chr((0xC0 & (ord($utf8{0}) << 6))
213
                         | (0x3F & ord($utf8{1})));
214

    
215
      case 3:
216
        // Return a UTF-16 character from a 3-byte UTF-8 char.
217
        // @see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
218
        return chr((0xF0 & (ord($utf8{0}) << 4))
219
                         | (0x0F & (ord($utf8{1}) >> 2)))
220
                     . chr((0xC0 & (ord($utf8{1}) << 6))
221
                         | (0x7F & ord($utf8{2})));
222
    }
223

    
224
    // Ignoring UTF-32 for now, sorry.
225
    return '';
226
  }
227

    
228
  /**
229
   * Encodes an arbitrary variable into JSON format.
230
   *
231
   * @access public
232
   *
233
   * @param mixed $var
234
   *   Any number, boolean, string, array, or object to be encoded.
235
   *   See argument 1 to Services_JSON() above for array-parsing behavior.
236
   *   If var is a string, note that encode() always expects it
237
   *   to be in ASCII or UTF-8 format!
238
   *
239
   * @return mixed
240
   *   JSON string representation of input var or an error if a problem occurs.
241
   */
242
  function encode($var) {
243
    switch (gettype($var)) {
244
      case 'boolean':
245
        return $var ? 'TRUE' : 'FALSE';
246

    
247
      case 'NULL':
248
        return 'NULL';
249

    
250
      case 'integer':
251
        return (int) $var;
252

    
253
      case 'double':
254
      case 'float':
255
        return (float) $var;
256

    
257
      case 'string':
258
        // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT.
259
        $ascii = '';
260
        $strlen_var = strlen($var);
261

    
262
        /*
263
         * Iterate over every character in the string,
264
         * escaping with a slash or encoding to UTF-8 where necessary.
265
         */
266
        for ($c = 0; $c < $strlen_var; ++$c) {
267

    
268
          $ord_var_c = ord($var{$c});
269

    
270
          switch (TRUE) {
271
            case $ord_var_c == 0x08:
272
              $ascii .= '\b';
273
              break;
274

    
275
            case $ord_var_c == 0x09:
276
              $ascii .= '\t';
277
              break;
278

    
279
            case $ord_var_c == 0x0A:
280
              $ascii .= '\n';
281
              break;
282

    
283
            case $ord_var_c == 0x0C:
284
              $ascii .= '\f';
285
              break;
286

    
287
            case $ord_var_c == 0x0D:
288
              $ascii .= '\r';
289
              break;
290

    
291
            case $ord_var_c == 0x22:
292
            case $ord_var_c == 0x2F:
293
            case $ord_var_c == 0x5C:
294
              // Double quote, slash, slosh.
295
              $ascii .= '\\' . $var{$c};
296
              break;
297

    
298
            case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)):
299
              // Characters U-00000000 - U-0000007F (same as ASCII).
300
              $ascii .= $var{$c};
301
              break;
302

    
303
            case (($ord_var_c & 0xE0) == 0xC0):
304
              // Characters U-00000080 - U-000007FF, mask 110XXXXX.
305
              // @see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
306
              $char = pack('C*', $ord_var_c, ord($var{$c + 1}));
307
              $c += 1;
308
              $utf16 = $this->utf82utf16($char);
309
              $ascii .= sprintf('\u%04s', bin2hex($utf16));
310
              break;
311

    
312
            case (($ord_var_c & 0xF0) == 0xE0):
313
              // Characters U-00000800 - U-0000FFFF, mask 1110XXXX.
314
              // @see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
315
              $char = pack('C*', $ord_var_c,
316
                                         ord($var{$c + 1}),
317
                                         ord($var{$c + 2}));
318
              $c += 2;
319
              $utf16 = $this->utf82utf16($char);
320
              $ascii .= sprintf('\u%04s', bin2hex($utf16));
321
              break;
322

    
323
            case (($ord_var_c & 0xF8) == 0xF0):
324
              // Characters U-00010000 - U-001FFFFF, mask 11110XXX.
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
                                         ord($var{$c + 3}));
330
              $c += 3;
331
              $utf16 = $this->utf82utf16($char);
332
              $ascii .= sprintf('\u%04s', bin2hex($utf16));
333
              break;
334

    
335
            case (($ord_var_c & 0xFC) == 0xF8):
336
              // Characters U-00200000 - U-03FFFFFF, mask 111110XX.
337
              // @see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
338
              $char = pack('C*', $ord_var_c,
339
                                         ord($var{$c + 1}),
340
                                         ord($var{$c + 2}),
341
                                         ord($var{$c + 3}),
342
                                         ord($var{$c + 4}));
343
              $c += 4;
344
              $utf16 = $this->utf82utf16($char);
345
              $ascii .= sprintf('\u%04s', bin2hex($utf16));
346
              break;
347

    
348
            case (($ord_var_c & 0xFE) == 0xFC):
349
              // Characters U-04000000 - U-7FFFFFFF, mask 1111110X.
350
              // @see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
351
              $char = pack('C*', $ord_var_c,
352
                                         ord($var{$c + 1}),
353
                                         ord($var{$c + 2}),
354
                                         ord($var{$c + 3}),
355
                                         ord($var{$c + 4}),
356
                                         ord($var{$c + 5}));
357
              $c += 5;
358
              $utf16 = $this->utf82utf16($char);
359
              $ascii .= sprintf('\u%04s', bin2hex($utf16));
360
              break;
361
          }
362
        }
363

    
364
        return '"' . $ascii . '"';
365

    
366
      case 'array':
367
        /*
368
        As per JSON spec if any array key is not an integer
369
        we must treat the the whole array as an object. We
370
        also try to catch a sparsely populated associative
371
        array with numeric keys here because some JS engines
372
        will create an array with empty indexes up to
373
        max_index which can cause memory issues and because
374
        the keys, which may be relevant, will be remapped
375
        otherwise.
376

    
377
        As per the ECMA and JSON specification an object may
378
        have any string as a property. Unfortunately due to
379
        a hole in the ECMA specification if the key is a
380
        ECMA reserved word or starts with a digit the
381
        parameter is only accessible using ECMAScript's
382
        bracket notation.
383
         */
384

    
385
        // Treat as a JSON object.
386
        if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) {
387
          $properties = array_map(array($this, 'name_value'),
388
                                            array_keys($var),
389
                                            array_values($var));
390

    
391
          foreach ($properties as $property) {
392
            if (Services_JSON::isError($property)) {
393
              return $property;
394
            }
395
          }
396

    
397
          return '{' . join(',', $properties) . '}';
398
        }
399

    
400
        // Treat it like a regular array.
401
        $elements = array_map(array($this, 'encode'), $var);
402

    
403
        foreach ($elements as $element) {
404
          if (Services_JSON::isError($element)) {
405
            return $element;
406
          }
407
        }
408

    
409
        return '[' . join(',', $elements) . ']';
410

    
411
      case 'object':
412
        $vars = get_object_vars($var);
413

    
414
        $properties = array_map(array($this, 'name_value'),
415
                                        array_keys($vars),
416
                                        array_values($vars));
417

    
418
        foreach ($properties as $property) {
419
          if (Services_JSON::isError($property)) {
420
            return $property;
421
          }
422
        }
423

    
424
        return '{' . join(',', $properties) . '}';
425

    
426
      default:
427
        return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS)
428
                    ? 'null'
429
                    : new Services_JSON_Error(gettype($var) . " can not be encoded as JSON string");
430
    }
431
  }
432

    
433
  /**
434
   * Array-walking function used in generating JSON-formatted name-value pairs.
435
   *
436
   * @access private
437
   *
438
   * @param string $name
439
   *   Name of key to use.
440
   * @param mixed $value
441
   *   Reference to an array element to be encoded.
442
   *
443
   * @return string
444
   *   JSON-formatted name-value pair, like '"name":value'.
445
   */
446
  function name_value($name, $value) {
447
    $encoded_value = $this->encode($value);
448

    
449
    if (Services_JSON::isError($encoded_value)) {
450
      return $encoded_value;
451
    }
452

    
453
    return $this->encode(strval($name)) . ':' . $encoded_value;
454
  }
455

    
456
  /**
457
   * Reduce a string by removing leading and trailing comments and whitespace.
458
   *
459
   * @access private
460
   *
461
   * @param string $str
462
   *   String value to strip of comments and whitespace.
463
   *
464
   * @return string
465
   *   String value stripped of comments and whitespace.array.
466
   */
467
  function reduce_string($str) {
468
    $str = preg_replace(array(
469
      // Eliminate single line comments in '// ...' form.
470
      '#^\s*//(.+)$#m',
471
      // Eliminate multi-line comments in '/* ... */' form, at start of string
472
      '#^\s*/\*(.+)\*/#Us',
473
      // Eliminate multi-line comments in '/* ... */' form, at end of string.
474
      '#/\*(.+)\*/\s*$#Us',
475
    ), '', $str);
476

    
477
    // Eliminate extraneous space.
478
    return trim($str);
479
  }
480

    
481
  /**
482
   * Decodes a JSON string into an appropriate variable.
483
   *
484
   * See argument 1 to Services_JSON() for object-output behavior.
485
   * Note that decode() always returns strings in ASCII or UTF-8 format!
486
   *
487
   * @access public
488
   *
489
   * @param string $str
490
   *   JSON-formatted string
491
   *
492
   * @return mixed
493
   *   Number, boolean, string, array, or object corresponding to given
494
   *   JSON input string.
495
   */
496
  function decode($str) {
497
    $str = $this->reduce_string($str);
498

    
499
    switch (strtolower($str)) {
500
      case 'true':
501
        return TRUE;
502

    
503
      case 'false':
504
        return FALSE;
505

    
506
      case 'null':
507
        return NULL;
508

    
509
      default:
510
        $m = array();
511

    
512
        if (is_numeric($str)) {
513
          // Lookie-loo, it's a number.
514
          // This would work on its own, but I'm trying to be
515
          // good about returning integers where appropriate:
516
          // return (float)$str;
517
          // Return float or int, as appropriate.
518
          return ((float) $str == (integer) $str)
519
                        ? (integer) $str
520
                        : (float) $str;
521

    
522
        }
523
        elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) {
524
          // STRINGS RETURNED IN UTF-8 FORMAT.
525
          $delim = substr($str, 0, 1);
526
          $chrs = substr($str, 1, -1);
527
          $utf8 = '';
528
          $strlen_chrs = strlen($chrs);
529

    
530
          for ($c = 0; $c < $strlen_chrs; ++$c) {
531

    
532
            $substr_chrs_c_2 = substr($chrs, $c, 2);
533
            $ord_chrs_c = ord($chrs{$c});
534

    
535
            switch (TRUE) {
536
              case $substr_chrs_c_2 == '\b':
537
                $utf8 .= chr(0x08);
538
                ++$c;
539
                break;
540

    
541
              case $substr_chrs_c_2 == '\t':
542
                $utf8 .= chr(0x09);
543
                ++$c;
544
                break;
545

    
546
              case $substr_chrs_c_2 == '\n':
547
                $utf8 .= chr(0x0A);
548
                ++$c;
549
                break;
550

    
551
              case $substr_chrs_c_2 == '\f':
552
                $utf8 .= chr(0x0C);
553
                ++$c;
554
                break;
555

    
556
              case $substr_chrs_c_2 == '\r':
557
                $utf8 .= chr(0x0D);
558
                ++$c;
559
                break;
560

    
561
              case $substr_chrs_c_2 == '\\"':
562
              case $substr_chrs_c_2 == '\\\'':
563
              case $substr_chrs_c_2 == '\\\\':
564
              case $substr_chrs_c_2 == '\\/':
565
                if (($delim == '"' && $substr_chrs_c_2 != '\\\'') ||
566
                                   ($delim == "'" && $substr_chrs_c_2 != '\\"')) {
567
                  $utf8 .= $chrs{++$c};
568
                }
569
                break;
570

    
571
              case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)):
572
                // Single, escaped unicode character.
573
                $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2)))
574
                                       . chr(hexdec(substr($chrs, ($c + 4), 2)));
575
                $utf8 .= $this->utf162utf8($utf16);
576
                $c += 5;
577
                break;
578

    
579
              case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F):
580
                $utf8 .= $chrs{$c};
581
                break;
582

    
583
              case ($ord_chrs_c & 0xE0) == 0xC0:
584
                // Characters U-00000080 - U-000007FF, mask 110XXXXX.
585
                // @see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
586
                $utf8 .= substr($chrs, $c, 2);
587
                ++$c;
588
                break;
589

    
590
              case ($ord_chrs_c & 0xF0) == 0xE0:
591
                // Characters U-00000800 - U-0000FFFF, mask 1110XXXX.
592
                // @see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
593
                $utf8 .= substr($chrs, $c, 3);
594
                $c += 2;
595
                break;
596

    
597
              case ($ord_chrs_c & 0xF8) == 0xF0:
598
                // Characters U-00010000 - U-001FFFFF, mask 11110XXX.
599
                // @see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
600
                $utf8 .= substr($chrs, $c, 4);
601
                $c += 3;
602
                break;
603

    
604
              case ($ord_chrs_c & 0xFC) == 0xF8:
605
                // Characters U-00200000 - U-03FFFFFF, mask 111110XX.
606
                // @see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
607
                $utf8 .= substr($chrs, $c, 5);
608
                $c += 4;
609
                break;
610

    
611
              case ($ord_chrs_c & 0xFE) == 0xFC:
612
                // Characters U-04000000 - U-7FFFFFFF, mask 1111110X.
613
                // @see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
614
                $utf8 .= substr($chrs, $c, 6);
615
                $c += 5;
616
                break;
617
            }
618
          }
619

    
620
          return $utf8;
621

    
622
        }
623
        elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) {
624
          // Array, or object notation.
625
          if ($str{0} == '[') {
626
            $stk = array(SERVICES_JSON_IN_ARR);
627
            $arr = array();
628
          }
629
          else {
630
            if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
631
              $stk = array(SERVICES_JSON_IN_OBJ);
632
              $obj = array();
633
            }
634
            else {
635
              $stk = array(SERVICES_JSON_IN_OBJ);
636
              $obj = new stdClass();
637
            }
638
          }
639

    
640
          array_push($stk, array(
641
            'what' => SERVICES_JSON_SLICE,
642
            'where' => 0,
643
            'delim' => FALSE,
644
          ));
645

    
646
          $chrs = substr($str, 1, -1);
647
          $chrs = $this->reduce_string($chrs);
648

    
649
          if ($chrs == '') {
650
            if (reset($stk) == SERVICES_JSON_IN_ARR) {
651
              return $arr;
652
            }
653
            else {
654
              return $obj;
655
            }
656
          }
657

    
658
          // print("\nparsing {$chrs}\n");
659
          $strlen_chrs = strlen($chrs);
660

    
661
          for ($c = 0; $c <= $strlen_chrs; ++$c) {
662
            $top = end($stk);
663
            $substr_chrs_c_2 = substr($chrs, $c, 2);
664

    
665
            if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) {
666
              // Found a comma that is not inside a string, array, etc.,
667
              // OR we've reached the end of the character list.
668
              $slice = substr($chrs, $top['where'], ($c - $top['where']));
669
              array_push($stk, array(
670
                'what' => SERVICES_JSON_SLICE,
671
                'where' => ($c + 1),
672
                'delim' => FALSE,
673
                ));
674

    
675
              // print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
676
              if (reset($stk) == SERVICES_JSON_IN_ARR) {
677
                // We are in an array, so just push an element onto the stack.
678
                array_push($arr, $this->decode($slice));
679
              }
680
              elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
681
                // We are in an object, so figure
682
                // out the property name and set an
683
                // element in an associative array,
684
                // for now.
685
                $parts = array();
686

    
687
                if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
688
                  // "name":value pair.
689
                  $key = $this->decode($parts[1]);
690
                  $val = $this->decode($parts[2]);
691

    
692
                  if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
693
                    $obj[$key] = $val;
694
                  }
695
                  else {
696
                    $obj->$key = $val;
697
                  }
698
                }
699
                elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) {
700
                  // Name:value pair, where name is unquoted.
701
                  $key = $parts[1];
702
                  $val = $this->decode($parts[2]);
703

    
704
                  if ($this->use & SERVICES_JSON_LOOSE_TYPE) {
705
                    $obj[$key] = $val;
706
                  }
707
                  else {
708
                    $obj->$key = $val;
709
                  }
710
                }
711
              }
712
            }
713
            elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) {
714
              // Found a quote, and we are not inside a string.
715
              array_push($stk, array(
716
                'what' => SERVICES_JSON_IN_STR,
717
                'where' => $c,
718
                'delim' => $chrs{$c},
719
                ));
720
              // print("Found start of string at {$c}\n");
721
            }
722
            elseif (($chrs{$c} == $top['delim']) &&
723
                                 ($top['what'] == SERVICES_JSON_IN_STR) &&
724
                                 (($chrs{$c - 1} != '\\') ||
725
                                 ($chrs{$c - 1} == '\\' && $chrs{$c - 2} == '\\'))) {
726
              // Found a quote, we're in a string, and it's not escaped.
727
              array_pop($stk);
728
              // print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n");
729

    
730
            }
731
            elseif (($chrs{$c} == '[') && in_array($top['what'], array(
732
                SERVICES_JSON_SLICE,
733
                SERVICES_JSON_IN_ARR,
734
                SERVICES_JSON_IN_OBJ,
735
                ))) {
736
              // Fund a left-bracket, and we are in an array, object, or slice.
737
              array_push($stk, array(
738
                'what' => SERVICES_JSON_IN_ARR,
739
                'where' => $c,
740
                'delim' => FALSE,
741
                ));
742
              // print("Found start of array at {$c}\n");
743

    
744
            }
745
            elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) {
746
              // Found a right-bracket, and we're in an array.
747
              array_pop($stk);
748
              // print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
749
            }
750
            elseif (($chrs{$c} == '{') &&
751
                                 in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
752
              // Found a left-brace, and we are in an array, object, or slice.
753
              array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => FALSE));
754
              // print("Found start of object at {$c}\n");
755
            }
756
            elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) {
757
              // Found a right-brace, and we're in an object.
758
              array_pop($stk);
759
              // print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
760
            }
761
            elseif (($substr_chrs_c_2 == '/*') &&
762
                                 in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) {
763
              // Found a comment start, and we are in an array, object, or slice.
764
              array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => FALSE));
765
              $c++;
766
              // print("Found start of comment at {$c}\n");
767
            }
768
            elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) {
769
              // Found a comment end, and we're in one now.
770
              array_pop($stk);
771
              $c++;
772

    
773
              for ($i = $top['where']; $i <= $c; ++$i) {
774
                $chrs = substr_replace($chrs, ' ', $i, 1);
775
              }
776
              // print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n");
777
            }
778
          }
779

    
780
          if (reset($stk) == SERVICES_JSON_IN_ARR) {
781
            return $arr;
782
          }
783
          elseif (reset($stk) == SERVICES_JSON_IN_OBJ) {
784
            return $obj;
785
          }
786
        }
787
    }
788
  }
789

    
790
  /**
791
   * @todo Ultimately, this should just call PEAR::isError().
792
   */
793
  function isError($data, $code = NULL) {
794
    if (class_exists('pear')) {
795
      return PEAR::isError($data, $code);
796
    }
797
    elseif (is_object($data) && (get_class($data) == 'services_json_error' ||
798
                                 is_subclass_of($data, 'services_json_error'))) {
799
      return TRUE;
800
    }
801

    
802
    return FALSE;
803
  }
804
}
805

    
806
if (class_exists('PEAR_Error')) {
807

    
808
  class Services_JSON_Error extends PEAR_Error {
809
    /**
810
     * @todo document this function.
811
     *
812
     * @param unknown_type $message
813
     * @param unknown_type $code
814
     * @param unknown_type $mode
815
     * @param unknown_type $options
816
     * @param unknown_type $userinfo
817
     */
818
    function __construct($message = 'unknown error', $code = NULL,
819
    $mode = NULL, $options = NULL, $userinfo = NULL) {
820
      parent::__construct($message, $code, $mode, $options, $userinfo);
821
    }
822
  }
823

    
824
}
825
else {
826
  /**
827
   * @todo Ultimately, this class shall be descended from PEAR_Error.
828
   */
829
  class Services_JSON_Error {
830
    /**
831
     * @todo document this function
832
     *
833
     * @param unknown_type $message
834
     * @param unknown_type $code
835
     * @param unknown_type $mode
836
     * @param unknown_type $options
837
     * @param unknown_type $userinfo
838
     */
839
    function __construct($message = 'unknown error', $code = NULL,
840
    $mode = NULL, $options = NULL, $userinfo = NULL) {
841

    
842
    }
843
  }
844
}
(1-1/2)