]>
Commit | Line | Data |
---|---|---|
964dd0bc JW |
1 | <?php |
2 | /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ | |
3 | ||
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,v 1.31 2006/06/28 05:54:17 migurski Exp $ | |
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 | /* modified for pitchfork to use echo instead of recursing over all elements and returning a *large* string */ | |
228 | /* could probably remove the conversion thing as input is utf-8 and output is utf-8 */ | |
229 | /** | |
230 | * encodes an arbitrary variable into JSON format | |
231 | * | |
232 | * @param mixed $var any number, boolean, string, array, or object to be encoded. | |
233 | * see argument 1 to Services_JSON() above for array-parsing behavior. | |
234 | * if var is a strng, note that encode() always expects it | |
235 | * to be in ASCII or UTF-8 format! | |
236 | * | |
237 | * @return mixed JSON string representation of input var or an error if a problem occurs | |
238 | * @access public | |
239 | */ | |
240 | function encode($var) | |
241 | { | |
242 | switch (gettype($var)) { | |
243 | case 'boolean': | |
244 | echo $var ? 'true' : 'false'; | |
245 | return; | |
246 | ||
247 | case 'NULL': | |
248 | echo 'null'; | |
249 | return; | |
250 | ||
251 | case 'integer': | |
252 | echo (int) $var; | |
253 | return; | |
254 | ||
255 | case 'double': | |
256 | case 'float': | |
257 | echo (float) $var; | |
258 | return; | |
259 | ||
260 | case 'string': | |
261 | // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT | |
262 | $ascii = ''; | |
263 | $strlen_var = strlen($var); | |
264 | ||
265 | /* | |
266 | * Iterate over every character in the string, | |
267 | * escaping with a slash or encoding to UTF-8 where necessary | |
268 | */ | |
269 | for ($c = 0; $c < $strlen_var; ++$c) { | |
270 | ||
271 | $ord_var_c = ord($var{$c}); | |
272 | ||
273 | switch (true) { | |
274 | case $ord_var_c == 0x08: | |
275 | $ascii .= '\b'; | |
276 | break; | |
277 | case $ord_var_c == 0x09: | |
278 | $ascii .= '\t'; | |
279 | break; | |
280 | case $ord_var_c == 0x0A: | |
281 | $ascii .= '\n'; | |
282 | break; | |
283 | case $ord_var_c == 0x0C: | |
284 | $ascii .= '\f'; | |
285 | break; | |
286 | case $ord_var_c == 0x0D: | |
287 | $ascii .= '\r'; | |
288 | break; | |
289 | ||
290 | case $ord_var_c == 0x22: | |
291 | case $ord_var_c == 0x2F: | |
292 | case $ord_var_c == 0x5C: | |
293 | // double quote, slash, slosh | |
294 | $ascii .= '\\'.$var{$c}; | |
295 | break; | |
296 | ||
297 | case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)): | |
298 | // characters U-00000000 - U-0000007F (same as ASCII) | |
299 | $ascii .= $var{$c}; | |
300 | break; | |
301 | ||
302 | case (($ord_var_c & 0xE0) == 0xC0): | |
303 | // characters U-00000080 - U-000007FF, mask 110XXXXX | |
304 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 | |
305 | $char = pack('C*', $ord_var_c, ord($var{$c + 1})); | |
306 | $c += 1; | |
307 | $utf16 = $this->utf82utf16($char); | |
308 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); | |
309 | break; | |
310 | ||
311 | case (($ord_var_c & 0xF0) == 0xE0): | |
312 | // characters U-00000800 - U-0000FFFF, mask 1110XXXX | |
313 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 | |
314 | $char = pack('C*', $ord_var_c, | |
315 | ord($var{$c + 1}), | |
316 | ord($var{$c + 2})); | |
317 | $c += 2; | |
318 | $utf16 = $this->utf82utf16($char); | |
319 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); | |
320 | break; | |
321 | ||
322 | case (($ord_var_c & 0xF8) == 0xF0): | |
323 | // characters U-00010000 - U-001FFFFF, mask 11110XXX | |
324 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 | |
325 | $char = pack('C*', $ord_var_c, | |
326 | ord($var{$c + 1}), | |
327 | ord($var{$c + 2}), | |
328 | ord($var{$c + 3})); | |
329 | $c += 3; | |
330 | $utf16 = $this->utf82utf16($char); | |
331 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); | |
332 | break; | |
333 | ||
334 | case (($ord_var_c & 0xFC) == 0xF8): | |
335 | // characters U-00200000 - U-03FFFFFF, mask 111110XX | |
336 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 | |
337 | $char = pack('C*', $ord_var_c, | |
338 | ord($var{$c + 1}), | |
339 | ord($var{$c + 2}), | |
340 | ord($var{$c + 3}), | |
341 | ord($var{$c + 4})); | |
342 | $c += 4; | |
343 | $utf16 = $this->utf82utf16($char); | |
344 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); | |
345 | break; | |
346 | ||
347 | case (($ord_var_c & 0xFE) == 0xFC): | |
348 | // characters U-04000000 - U-7FFFFFFF, mask 1111110X | |
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 | ord($var{$c + 3}), | |
354 | ord($var{$c + 4}), | |
355 | ord($var{$c + 5})); | |
356 | $c += 5; | |
357 | $utf16 = $this->utf82utf16($char); | |
358 | $ascii .= sprintf('\u%04s', bin2hex($utf16)); | |
359 | break; | |
360 | } | |
361 | } | |
362 | ||
363 | echo '"'.$ascii.'"'; | |
364 | return; | |
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 | echo "{"; | |
388 | $first = true; | |
389 | foreach($var as $k => $v) { | |
390 | if($first) $first = false; | |
391 | else echo ","; | |
392 | $this->name_value($k, $v); | |
393 | } | |
394 | ||
395 | echo '}'; | |
396 | return; | |
397 | } | |
398 | ||
399 | // treat it like a regular array | |
400 | echo "["; | |
401 | $first = true; | |
402 | foreach($var as $e) { | |
403 | if($first) $first = false; | |
404 | else echo ","; | |
405 | $this->encode($e); | |
406 | } | |
407 | echo "]"; | |
408 | return; | |
409 | ||
410 | case 'object': | |
411 | echo "{"; | |
412 | $first = true; | |
413 | foreach($var as $k => $v) { | |
414 | if($first) | |
415 | $first = false; | |
416 | else echo ","; | |
417 | $this->name_value($k, $v); | |
418 | } | |
419 | ||
420 | echo '}'; | |
421 | return; | |
422 | ||
423 | default: | |
424 | echo ($this->use & SERVICES_JSON_SUPPRESS_ERRORS) | |
425 | ? 'null' | |
426 | : 'error encoding'; | |
427 | return; | |
428 | } | |
429 | } | |
430 | ||
431 | /** | |
432 | * array-walking function for use in generating JSON-formatted name-value pairs | |
433 | * | |
434 | * @param string $name name of key to use | |
435 | * @param mixed $value reference to an array element to be encoded | |
436 | * | |
437 | * @return string JSON-formatted name-value pair, like '"name":value' | |
438 | * @access private | |
439 | */ | |
440 | function name_value($name, $value) | |
441 | { | |
442 | $this->encode(strval($name)); | |
443 | echo ':'; | |
444 | $this->encode($value); | |
445 | } | |
446 | ||
447 | /** | |
448 | * reduce a string by removing leading and trailing comments and whitespace | |
449 | * | |
450 | * @param $str string string value to strip of comments and whitespace | |
451 | * | |
452 | * @return string string value stripped of comments and whitespace | |
453 | * @access private | |
454 | */ | |
455 | function reduce_string($str) | |
456 | { | |
457 | $str = preg_replace(array( | |
458 | ||
459 | // eliminate single line comments in '// ...' form | |
460 | '#^\s*//(.+)$#m', | |
461 | ||
462 | // eliminate multi-line comments in '/* ... */' form, at start of string | |
463 | '#^\s*/\*(.+)\*/#Us', | |
464 | ||
465 | // eliminate multi-line comments in '/* ... */' form, at end of string | |
466 | '#/\*(.+)\*/\s*$#Us' | |
467 | ||
468 | ), '', $str); | |
469 | ||
470 | // eliminate extraneous space | |
471 | return trim($str); | |
472 | } | |
473 | ||
474 | /** | |
475 | * decodes a JSON string into appropriate variable | |
476 | * | |
477 | * @param string $str JSON-formatted string | |
478 | * | |
479 | * @return mixed number, boolean, string, array, or object | |
480 | * corresponding to given JSON input string. | |
481 | * See argument 1 to Services_JSON() above for object-output behavior. | |
482 | * Note that decode() always returns strings | |
483 | * in ASCII or UTF-8 format! | |
484 | * @access public | |
485 | */ | |
486 | function decode($str) | |
487 | { | |
488 | $str = $this->reduce_string($str); | |
489 | ||
490 | switch (strtolower($str)) { | |
491 | case 'true': | |
492 | return true; | |
493 | ||
494 | case 'false': | |
495 | return false; | |
496 | ||
497 | case 'null': | |
498 | return null; | |
499 | ||
500 | default: | |
501 | $m = array(); | |
502 | ||
503 | if (is_numeric($str)) { | |
504 | // Lookie-loo, it's a number | |
505 | ||
506 | // This would work on its own, but I'm trying to be | |
507 | // good about returning integers where appropriate: | |
508 | // return (float)$str; | |
509 | ||
510 | // Return float or int, as appropriate | |
511 | return ((float)$str == (integer)$str) | |
512 | ? (integer)$str | |
513 | : (float)$str; | |
514 | ||
515 | } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) { | |
516 | // STRINGS RETURNED IN UTF-8 FORMAT | |
517 | $delim = substr($str, 0, 1); | |
518 | $chrs = substr($str, 1, -1); | |
519 | $utf8 = ''; | |
520 | $strlen_chrs = strlen($chrs); | |
521 | ||
522 | for ($c = 0; $c < $strlen_chrs; ++$c) { | |
523 | ||
524 | $substr_chrs_c_2 = substr($chrs, $c, 2); | |
525 | $ord_chrs_c = ord($chrs{$c}); | |
526 | ||
527 | switch (true) { | |
528 | case $substr_chrs_c_2 == '\b': | |
529 | $utf8 .= chr(0x08); | |
530 | ++$c; | |
531 | break; | |
532 | case $substr_chrs_c_2 == '\t': | |
533 | $utf8 .= chr(0x09); | |
534 | ++$c; | |
535 | break; | |
536 | case $substr_chrs_c_2 == '\n': | |
537 | $utf8 .= chr(0x0A); | |
538 | ++$c; | |
539 | break; | |
540 | case $substr_chrs_c_2 == '\f': | |
541 | $utf8 .= chr(0x0C); | |
542 | ++$c; | |
543 | break; | |
544 | case $substr_chrs_c_2 == '\r': | |
545 | $utf8 .= chr(0x0D); | |
546 | ++$c; | |
547 | break; | |
548 | ||
549 | case $substr_chrs_c_2 == '\\"': | |
550 | case $substr_chrs_c_2 == '\\\'': | |
551 | case $substr_chrs_c_2 == '\\\\': | |
552 | case $substr_chrs_c_2 == '\\/': | |
553 | if (($delim == '"' && $substr_chrs_c_2 != '\\\'') || | |
554 | ($delim == "'" && $substr_chrs_c_2 != '\\"')) { | |
555 | $utf8 .= $chrs{++$c}; | |
556 | } | |
557 | break; | |
558 | ||
559 | case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)): | |
560 | // single, escaped unicode character | |
561 | $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) | |
562 | . chr(hexdec(substr($chrs, ($c + 4), 2))); | |
563 | $utf8 .= $this->utf162utf8($utf16); | |
564 | $c += 5; | |
565 | break; | |
566 | ||
567 | case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F): | |
568 | $utf8 .= $chrs{$c}; | |
569 | break; | |
570 | ||
571 | case ($ord_chrs_c & 0xE0) == 0xC0: | |
572 | // characters U-00000080 - U-000007FF, mask 110XXXXX | |
573 | //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 | |
574 | $utf8 .= substr($chrs, $c, 2); | |
575 | ++$c; | |
576 | break; | |
577 | ||
578 | case ($ord_chrs_c & 0xF0) == 0xE0: | |
579 | // characters U-00000800 - U-0000FFFF, mask 1110XXXX | |
580 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 | |
581 | $utf8 .= substr($chrs, $c, 3); | |
582 | $c += 2; | |
583 | break; | |
584 | ||
585 | case ($ord_chrs_c & 0xF8) == 0xF0: | |
586 | // characters U-00010000 - U-001FFFFF, mask 11110XXX | |
587 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 | |
588 | $utf8 .= substr($chrs, $c, 4); | |
589 | $c += 3; | |
590 | break; | |
591 | ||
592 | case ($ord_chrs_c & 0xFC) == 0xF8: | |
593 | // characters U-00200000 - U-03FFFFFF, mask 111110XX | |
594 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 | |
595 | $utf8 .= substr($chrs, $c, 5); | |
596 | $c += 4; | |
597 | break; | |
598 | ||
599 | case ($ord_chrs_c & 0xFE) == 0xFC: | |
600 | // characters U-04000000 - U-7FFFFFFF, mask 1111110X | |
601 | // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 | |
602 | $utf8 .= substr($chrs, $c, 6); | |
603 | $c += 5; | |
604 | break; | |
605 | ||
606 | } | |
607 | ||
608 | } | |
609 | ||
610 | return $utf8; | |
611 | ||
612 | } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) { | |
613 | // array, or object notation | |
614 | ||
615 | if ($str{0} == '[') { | |
616 | $stk = array(SERVICES_JSON_IN_ARR); | |
617 | $arr = array(); | |
618 | } else { | |
619 | if ($this->use & SERVICES_JSON_LOOSE_TYPE) { | |
620 | $stk = array(SERVICES_JSON_IN_OBJ); | |
621 | $obj = array(); | |
622 | } else { | |
623 | $stk = array(SERVICES_JSON_IN_OBJ); | |
624 | $obj = new stdClass(); | |
625 | } | |
626 | } | |
627 | ||
628 | array_push($stk, array('what' => SERVICES_JSON_SLICE, | |
629 | 'where' => 0, | |
630 | 'delim' => false)); | |
631 | ||
632 | $chrs = substr($str, 1, -1); | |
633 | $chrs = $this->reduce_string($chrs); | |
634 | ||
635 | if ($chrs == '') { | |
636 | if (reset($stk) == SERVICES_JSON_IN_ARR) { | |
637 | return $arr; | |
638 | ||
639 | } else { | |
640 | return $obj; | |
641 | ||
642 | } | |
643 | } | |
644 | ||
645 | //print("\nparsing {$chrs}\n"); | |
646 | ||
647 | $strlen_chrs = strlen($chrs); | |
648 | ||
649 | for ($c = 0; $c <= $strlen_chrs; ++$c) { | |
650 | ||
651 | $top = end($stk); | |
652 | $substr_chrs_c_2 = substr($chrs, $c, 2); | |
653 | ||
654 | if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) { | |
655 | // found a comma that is not inside a string, array, etc., | |
656 | // OR we've reached the end of the character list | |
657 | $slice = substr($chrs, $top['where'], ($c - $top['where'])); | |
658 | array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false)); | |
659 | //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); | |
660 | ||
661 | if (reset($stk) == SERVICES_JSON_IN_ARR) { | |
662 | // we are in an array, so just push an element onto the stack | |
663 | array_push($arr, $this->decode($slice)); | |
664 | ||
665 | } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { | |
666 | // we are in an object, so figure | |
667 | // out the property name and set an | |
668 | // element in an associative array, | |
669 | // for now | |
670 | $parts = array(); | |
671 | ||
672 | if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { | |
673 | // "name":value pair | |
674 | $key = $this->decode($parts[1]); | |
675 | $val = $this->decode($parts[2]); | |
676 | ||
677 | if ($this->use & SERVICES_JSON_LOOSE_TYPE) { | |
678 | $obj[$key] = $val; | |
679 | } else { | |
680 | $obj->$key = $val; | |
681 | } | |
682 | } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { | |
683 | // name:value pair, where name is unquoted | |
684 | $key = $parts[1]; | |
685 | $val = $this->decode($parts[2]); | |
686 | ||
687 | if ($this->use & SERVICES_JSON_LOOSE_TYPE) { | |
688 | $obj[$key] = $val; | |
689 | } else { | |
690 | $obj->$key = $val; | |
691 | } | |
692 | } | |
693 | ||
694 | } | |
695 | ||
696 | } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) { | |
697 | // found a quote, and we are not inside a string | |
698 | array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c})); | |
699 | //print("Found start of string at {$c}\n"); | |
700 | ||
701 | } elseif (($chrs{$c} == $top['delim']) && | |
702 | ($top['what'] == SERVICES_JSON_IN_STR) && | |
703 | ((strlen(substr($chrs, 0, $c)) - strlen(rtrim(substr($chrs, 0, $c), '\\'))) % 2 != 1)) { | |
704 | // found a quote, we're in a string, and it's not escaped | |
705 | // we know that it's not escaped becase there is _not_ an | |
706 | // odd number of backslashes at the end of the string so far | |
707 | array_pop($stk); | |
708 | //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n"); | |
709 | ||
710 | } elseif (($chrs{$c} == '[') && | |
711 | in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { | |
712 | // found a left-bracket, and we are in an array, object, or slice | |
713 | array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false)); | |
714 | //print("Found start of array at {$c}\n"); | |
715 | ||
716 | } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) { | |
717 | // found a right-bracket, and we're in an array | |
718 | array_pop($stk); | |
719 | //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); | |
720 | ||
721 | } elseif (($chrs{$c} == '{') && | |
722 | in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { | |
723 | // found a left-brace, and we are in an array, object, or slice | |
724 | array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false)); | |
725 | //print("Found start of object at {$c}\n"); | |
726 | ||
727 | } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) { | |
728 | // found a right-brace, and we're in an object | |
729 | array_pop($stk); | |
730 | //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); | |
731 | ||
732 | } elseif (($substr_chrs_c_2 == '/*') && | |
733 | in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { | |
734 | // found a comment start, and we are in an array, object, or slice | |
735 | array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false)); | |
736 | $c++; | |
737 | //print("Found start of comment at {$c}\n"); | |
738 | ||
739 | } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) { | |
740 | // found a comment end, and we're in one now | |
741 | array_pop($stk); | |
742 | $c++; | |
743 | ||
744 | for ($i = $top['where']; $i <= $c; ++$i) | |
745 | $chrs = substr_replace($chrs, ' ', $i, 1); | |
746 | ||
747 | //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); | |
748 | ||
749 | } | |
750 | ||
751 | } | |
752 | ||
753 | if (reset($stk) == SERVICES_JSON_IN_ARR) { | |
754 | return $arr; | |
755 | ||
756 | } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { | |
757 | return $obj; | |
758 | ||
759 | } | |
760 | ||
761 | } | |
762 | } | |
763 | } | |
764 | ||
765 | /** | |
766 | * @todo Ultimately, this should just call PEAR::isError() | |
767 | */ | |
768 | function isError($data, $code = null) | |
769 | { | |
770 | if (class_exists('pear')) { | |
771 | return PEAR::isError($data, $code); | |
772 | } elseif (is_object($data) && (get_class($data) == 'services_json_error' || | |
773 | is_subclass_of($data, 'services_json_error'))) { | |
774 | return true; | |
775 | } | |
776 | ||
777 | return false; | |
778 | } | |
779 | } | |
780 | ||
781 | if (class_exists('PEAR_Error')) { | |
782 | ||
783 | class Services_JSON_Error extends PEAR_Error | |
784 | { | |
785 | function Services_JSON_Error($message = 'unknown error', $code = null, | |
786 | $mode = null, $options = null, $userinfo = null) | |
787 | { | |
788 | parent::PEAR_Error($message, $code, $mode, $options, $userinfo); | |
789 | } | |
790 | } | |
791 | ||
792 | } else { | |
793 | ||
794 | /** | |
795 | * @todo Ultimately, this class shall be descended from PEAR_Error | |
796 | */ | |
797 | class Services_JSON_Error | |
798 | { | |
799 | function Services_JSON_Error($message = 'unknown error', $code = null, | |
800 | $mode = null, $options = null, $userinfo = null) | |
801 | { | |
802 | ||
803 | } | |
804 | } | |
805 | ||
806 | } | |
807 | ||
808 | ?> |