Project

General

Profile

Download (28.7 KB) Statistics
| Branch: | Tag: | Revision:
1 6657531f Andreas Kohlbecker
<?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
}