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