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