root/fJSON.php

Revision 848, 17.3 kB (checked in by wbond, 3 months ago)

Removed all e flags for preg_replace() and all calls to create_function() as a first step towards compatibility with Facebook's HipHop? for PHP

LineHide Line Numbers
1 <?php
2 /**
3  * Provides encoding and decoding for JSON
4  *
5  * This class is a compatibility class for the
6  * [http://php.net/json json extension] on servers with PHP 5.0 or 5.1, or
7  * servers with the json extension compiled out.
8  *
9  * This class will handle JSON values that are not contained in an array or
10  * object - such values are not valid according to the JSON spec, but the
11  * functionality is included for compatiblity with the json extension.
12  *
13  * @copyright  Copyright (c) 2008-2010 Will Bond
14  * @author     Will Bond [wb] <will@flourishlib.com>
15  * @license    http://flourishlib.com/license
16  *
17  * @package    Flourish
18  * @link       http://flourishlib.com/fJSON
19  *
20  * @version    1.0.0b6
21  * @changes    1.0.0b6  Removed `e` flag from preg_replace() calls [wb, 2010-06-08]
22  * @changes    1.0.0b5  Added the ::output() method [wb, 2010-03-15]
23  * @changes    1.0.0b4  Fixed a bug with ::decode() where JSON objects could lose all but the first key: value pair [wb, 2009-05-06]
24  * @changes    1.0.0b3  Updated the class to be consistent with PHP 5.2.9+ for encoding and decoding invalid data [wb, 2009-05-04]
25  * @changes    1.0.0b2  Changed @ error suppression operator to `error_reporting()` calls [wb, 2009-01-26]
26  * @changes    1.0.0b   The initial implementation [wb, 2008-07-12]
27  */
28 class fJSON
29 {
30     // The following constants allow for nice looking callbacks to static methods
31     const decode     = 'fJSON::decode';
32     const encode     = 'fJSON::encode';
33     const output     = 'fJSON::output';
34     const sendHeader = 'fJSON::sendHeader';
35    
36    
37     /**
38     * An abstract representation of [
39     *
40     * @internal
41     
42     * @var integer
43     */
44     const J_ARRAY_OPEN  = 0;
45    
46     /**
47     * An abstract representation of , in a JSON array
48     *
49     * @internal
50     
51     * @var integer
52     */
53     const J_ARRAY_COMMA = 1;
54    
55     /**
56     * An abstract representation of ]
57     *
58     * @internal
59     
60     * @var integer
61     */
62     const J_ARRAY_CLOSE = 2;
63    
64     /**
65     * An abstract representation of {
66     *
67     * @internal
68     
69     * @var integer
70     */
71     const J_OBJ_OPEN    = 3;
72    
73     /**
74     * An abstract representation of a JSON object key
75     *
76     * @internal
77     
78     * @var integer
79     */
80     const J_KEY         = 4;
81    
82     /**
83     * An abstract representation of :
84     *
85     * @internal
86     
87     * @var integer
88     */
89     const J_COLON       = 5;
90    
91     /**
92     * An abstract representation of , in a JSON object
93     *
94     * @internal
95     
96     * @var integer
97     */
98     const J_OBJ_COMMA   = 6;
99    
100     /**
101     * An abstract representation of }
102     *
103     * @internal
104     
105     * @var integer
106     */
107     const J_OBJ_CLOSE   = 7;
108    
109     /**
110     * An abstract representation of an integer
111     *
112     * @internal
113     
114     * @var integer
115     */
116     const J_INTEGER     = 8;
117    
118     /**
119     * An abstract representation of a floating value
120     *
121     * @internal
122     
123     * @var integer
124     */
125     const J_FLOAT       = 9;
126    
127     /**
128     * An abstract representation of a boolean true
129     *
130     * @internal
131     
132     * @var integer
133     */
134     const J_TRUE        = 10;
135    
136     /**
137     * An abstract representation of a boolean false
138     *
139     * @internal
140     
141     * @var integer
142     */
143     const J_FALSE       = 11;
144    
145     /**
146     * An abstract representation of null
147     *
148     * @internal
149     
150     * @var integer
151     */
152     const J_NULL        = 12;
153    
154     /**
155     * An abstract representation of a string
156     *
157     * @internal
158     
159     * @var integer
160     */
161     const J_STRING      = 13;
162    
163    
164     /**
165     * An array of special characters in JSON strings
166    
167     * @var array
168     */
169     static private $control_character_map = array(
170         '"'   => '\"', '\\' => '\\\\', '/'  => '\/', "\x8" => '\b',
171         "\xC" => '\f', "\n" => '\n',   "\r" => '\r', "\t"  => '\t'
172     );
173    
174     /**
175     * An array of what values are allowed after other values
176     *
177     * @internal
178     
179     * @var array
180     */
181     static private $next_values = array(
182         self::J_ARRAY_OPEN => array(
183             self::J_ARRAY_OPEN  => TRUE,
184             self::J_ARRAY_CLOSE => TRUE,
185             self::J_OBJ_OPEN    => TRUE,
186             self::J_INTEGER     => TRUE,
187             self::J_FLOAT       => TRUE,
188             self::J_TRUE        => TRUE,
189             self::J_FALSE       => TRUE,
190             self::J_NULL        => TRUE,
191             self::J_STRING      => TRUE
192         ),
193         self::J_ARRAY_COMMA => array(
194             self::J_ARRAY_OPEN  => TRUE,
195             self::J_OBJ_OPEN    => TRUE,
196             self::J_INTEGER     => TRUE,
197             self::J_FLOAT       => TRUE,
198             self::J_TRUE        => TRUE,
199             self::J_FALSE       => TRUE,
200             self::J_NULL        => TRUE,
201             self::J_STRING      => TRUE
202         ),
203         self::J_ARRAY_CLOSE => array(
204             self::J_ARRAY_CLOSE => TRUE,
205             self::J_OBJ_CLOSE   => TRUE,
206             self::J_ARRAY_COMMA => TRUE,
207             self::J_OBJ_COMMA   => TRUE
208         ),
209         self::J_OBJ_OPEN => array(
210             self::J_OBJ_CLOSE   => TRUE,
211             self::J_KEY         => TRUE
212         ),
213         self::J_KEY => array(
214             self::J_COLON       => TRUE
215         ),
216         self::J_OBJ_COMMA => array(
217             self::J_KEY         => TRUE
218         ),
219         self::J_COLON => array(
220             self::J_ARRAY_OPEN  => TRUE,
221             self::J_OBJ_OPEN    => TRUE,
222             self::J_INTEGER     => TRUE,
223             self::J_FLOAT       => TRUE,
224             self::J_TRUE        => TRUE,
225             self::J_FALSE       => TRUE,
226             self::J_NULL        => TRUE,
227             self::J_STRING      => TRUE
228         ),
229         self::J_OBJ_CLOSE => array(
230             self::J_ARRAY_CLOSE => TRUE,
231             self::J_OBJ_CLOSE   => TRUE,
232             self::J_ARRAY_COMMA => TRUE,
233             self::J_OBJ_COMMA   => TRUE
234         ),
235         self::J_INTEGER => array(
236             self::J_ARRAY_CLOSE => TRUE,
237             self::J_OBJ_CLOSE   => TRUE,
238             self::J_ARRAY_COMMA => TRUE,
239             self::J_OBJ_COMMA   => TRUE
240         ),
241         self::J_FLOAT => array(
242             self::J_ARRAY_CLOSE => TRUE,
243             self::J_OBJ_CLOSE   => TRUE,
244             self::J_ARRAY_COMMA => TRUE,
245             self::J_OBJ_COMMA   => TRUE
246         ),
247         self::J_TRUE => array(
248             self::J_ARRAY_CLOSE => TRUE,
249             self::J_OBJ_CLOSE   => TRUE,
250             self::J_ARRAY_COMMA => TRUE,
251             self::J_OBJ_COMMA   => TRUE
252         ),
253         self::J_FALSE => array(
254             self::J_ARRAY_CLOSE => TRUE,
255             self::J_OBJ_CLOSE   => TRUE,
256             self::J_ARRAY_COMMA => TRUE,
257             self::J_OBJ_COMMA   => TRUE
258         ),
259         self::J_NULL => array(
260             self::J_ARRAY_CLOSE => TRUE,
261             self::J_OBJ_CLOSE   => TRUE,
262             self::J_ARRAY_COMMA => TRUE,
263             self::J_OBJ_COMMA   => TRUE
264         ),
265         self::J_STRING => array(
266             self::J_ARRAY_CLOSE => TRUE,
267             self::J_OBJ_CLOSE   => TRUE,
268             self::J_ARRAY_COMMA => TRUE,
269             self::J_OBJ_COMMA   => TRUE
270         )
271     );
272    
273    
274     /**
275     * Decodes a JSON string into native PHP data types
276     *
277     * This function is very strict about the format of JSON. If the string is
278     * not a valid JSON string, `NULL` will be returned.
279    
280     * @param  string  $json   This should be the name of a related class
281     * @param  boolean $assoc  If this is TRUE, JSON objects will be represented as an assocative array instead of a `stdClass` object
282     * @return array|stdClass  A PHP equivalent of the JSON string
283     */
284     static public function decode($json, $assoc=FALSE)
285     {
286         if (!is_string($json) && !is_numeric($json)) {
287             return NULL;
288         }
289        
290         $json = trim($json);
291        
292         if ($json === '') {
293             return NULL;
294         }
295        
296         // If the json is an array or object, we can rely on the php function
297         if (function_exists('json_decode') && ($json[0] == '[' || $json[0] == '{' || version_compare(PHP_VERSION, '5.2.9', '>='))) {
298             return json_decode($json, $assoc);
299         }
300        
301         preg_match_all('~\[|                                     # Array begin
302                          \]|                                     # Array end
303                          {|                                         # Object begin
304                          }|                                         # Object end
305                          -?(?:0|[1-9]\d*)                        # Float
306                              (?:\.\d*(?:[eE][+\-]?\d+)?|
307                              (?:[eE][+\-]?\d+))|
308                          -?(?:0|[1-9]\d*)|                         # Integer
309                          true|                                     # True
310                          false|                                     # False
311                          null|                                     # Null
312                          ,|                                         # Member separator for arrays and objects
313                          :|                                         # Value separator for objects
314                          "(?:(?:(?!\\\\u)[^\\\\"\n\b\f\r\t]+)|   # String
315                              \\\\\\\\|
316                              \\\\/|
317                              \\\\"|
318                              \\\\b|
319                              \\\\f|
320                              \\\\n|
321                              \\\\r|
322                              \\\\t|
323                              \\\\u[0-9a-fA-F]{4})*"|
324                          \s+                                     # Whitespace
325                          ~x', $json, $matches);
326        
327         $matched_length = 0;
328         $stack          = array();
329         $last           = NULL;
330         $last_key       = NULL;
331         $output         = NULL;
332         $container      = NULL;
333        
334         if (sizeof($matches) == 1 && strlen($matches[0][0]) == strlen($json)) {
335             $match = $matches[0][0];
336             $stack = array();
337             $type  = self::getElementType($stack, self::J_ARRAY_OPEN, $match);
338             $element = self::scalarize($type, $match);
339             if ($match !== $element) {
340                 return $element;
341             }
342         }
343                            
344         if ($json[0] != '[' && $json[0] != '{') {
345             return NULL;
346         }
347        
348         foreach ($matches[0] as $match) {
349             if ($matched_length == 0) {
350                 if ($match == '[') {
351                     $output  = array();
352                     $last    = self::J_ARRAY_OPEN;
353                 } else {
354                     $output  = ($assoc) ? array() : new stdClass();
355                     $last    = self::J_OBJ_OPEN;
356                 }
357                 $stack[]   =  array($last, &$output);
358                 $container =& $output;
359                
360                 $matched_length = 1;
361            
362                 continue;
363             }
364            
365             $matched_length += strlen($match);
366            
367             // Whitespace can be skipped over
368             if (ctype_space($match)) {
369                 continue;
370             }
371            
372             $type = self::getElementType($stack, $last, $match);
373            
374             // An invalid sequence will cause parsing to stop
375             if (!isset(self::$next_values[$last][$type])) {
376                 break;
377             }
378            
379             // Decode the data values
380             $match = self::scalarize($type, $match);
381            
382             // If the element is not a value, record some info and continue
383             if ($type == self::J_COLON ||
384                   $type == self::J_OBJ_COMMA ||
385                   $type == self::J_ARRAY_COMMA ||
386                   $type == self::J_KEY) {
387                 $last = $type;
388                 if ($type == self::J_KEY) {
389                     $last_key = $match;
390                 }
391                 continue;
392             }
393            
394             // This flag is used to indicate if an array or object is being added and thus
395             // if the container reference needs to be changed to the current match
396             $ref_match = FALSE;
397            
398             // Closing an object or array
399             if ($type == self::J_OBJ_CLOSE || $type == self::J_ARRAY_CLOSE) {
400                 array_pop($stack);
401                 if (sizeof($stack) == 0) {
402                     break;
403                 }
404                 $new_container = end($stack);
405                 $container =& $new_container[1];
406                 $last = $type;
407                 continue;
408             }
409            
410             // Opening a new object or array requires some references to keep
411             // track of what the current container is
412             if ($type == self::J_OBJ_OPEN) {
413                 $match = ($assoc) ? array() : new stdClass();
414                 $ref_match = TRUE;
415             }
416             if ($type == self::J_ARRAY_OPEN) {
417                 $match = array();
418                 $ref_match = TRUE;
419             }
420            
421             if ($ref_match) {
422                 $stack[] = array($type, &$match);
423                 $stack_end = end($stack);
424             }
425            
426            
427             // Here we assign the value. This code is kind of crazy because
428             // we have to keep track of the current container by references
429             // so we can traverse back down the stack as we move out of
430             // nested arrays and objects
431             if ($last == self::J_COLON && !$assoc) {
432                 if ($last_key == '') {
433                     $last_key = '_empty_';
434                 }
435                 if ($ref_match) {
436                     $container->$last_key =& $stack_end[1];
437                     $container =& $stack_end[1];
438                 } else {
439                     $container->$last_key = $match;
440                 }
441                
442             } elseif ($last == self::J_COLON) {
443                 if ($ref_match) {
444                     $container[$last_key] =& $stack_end[1];
445                     $container =& $stack_end[1];
446                 } else {
447                     $container[$last_key] = $match;
448                 }
449                
450             } else {
451                 if ($ref_match) {
452                     $container[] =& $stack_end[1];
453                     $container =& $stack_end[1];
454                 } else {
455                     $container[] = $match;
456                 }
457             }
458            
459             if ($last == self::J_COLON) {
460                 $last_key = NULL;
461             }
462             $last = $type;
463             unset($match);
464         }
465        
466         if ($matched_length != strlen($json) || sizeof($stack) > 0) {
467             return NULL;
468         }
469        
470         return $output;
471     }
472    
473    
474     /**
475     * Encodes a PHP value into a JSON string
476     *
477     * @param  mixed  $value   The PHP value to encode
478     * @return string  The JSON string that is equivalent to the PHP value
479     */
480     static public function encode($value)
481     {
482         if (is_resource($value)) {
483             return 'null';
484         }
485        
486         if (function_exists('json_encode')) {
487             return json_encode($value);
488         }
489        
490         if (is_int($value)) {
491             return (string) $value;
492         }
493        
494         if (is_float($value)) {
495             return (string) $value;
496         }
497        
498         if (is_bool($value)) {
499             return ($value) ? 'true' : 'false';
500         }
501        
502         if (is_null($value)) {
503             return 'null';
504         }
505        
506         if (is_string($value)) {
507            
508             if (!preg_match('#^.*$#usD', $value)) {
509                 return 'null';
510             }
511            
512             $char_array = fUTF8::explode($value);
513            
514             $output = '"';
515             foreach ($char_array as $char) {
516                 if (isset(self::$control_character_map[$char])) {
517                     $output .= self::$control_character_map[$char];
518                
519                 } elseif (strlen($char) < 2) {
520                     $output .= $char;
521                
522                 } else {
523                     $output .= '\u' . substr(strtolower(fUTF8::ord($char)), 2);
524                 }
525             }
526             $output .= '"';
527            
528             return $output;
529         }
530        
531         // Detect if an array is associative, which would mean it needs to be encoded as an object
532         $is_assoc_array = FALSE;
533         if (is_array($value) && $value) {
534             $looking_for = 0;
535             foreach ($value as $key => $val) {
536                 if (!is_numeric($key) || $key != $looking_for) {
537                     $is_assoc_array = TRUE;
538                     break;
539                 }
540                 $looking_for++;
541             }
542         }
543        
544         if (is_object($value) || $is_assoc_array) {
545             $output  = '{';
546             $members = array();
547            
548             foreach ($value as $key => $val) {
549                 $members[] = self::encode((string) $key) . ':' . self::encode($val);
550             }
551            
552             $output .= join(',', $members);
553             $output .= '}';
554             return $output;
555         }
556        
557         if (is_array($value)) {
558             $output  = '[';
559             $members = array();
560            
561             foreach ($value as $key => $val) {
562                 $members[] = self::encode($val);
563             }
564            
565             $output .= join(',', $members);
566             $output .= ']';
567             return $output;
568         }
569     }
570    
571    
572     /**
573     * Determines the type of a parser JSON element
574    
575     * @param  array   &$stack   The stack of arrays/objects being parsed
576     * @param  integer $last     The type of the last element parsed
577     * @param  string  $element  The element being detected
578     * @return integer  The element type
579     */
580     static private function getElementType(&$stack, $last, $element)
581     {
582         if ($element == '[') {
583             return self::J_ARRAY_OPEN;
584         }
585        
586         if ($element == ']') {
587             return self::J_ARRAY_CLOSE;
588         }
589        
590         if ($element == '{') {
591             return self::J_OBJ_OPEN;
592         }
593        
594         if ($element == '}') {
595             return self::J_OBJ_CLOSE;
596         }
597        
598         if (ctype_digit($element)) {
599             return self::J_INTEGER;
600         }
601        
602         if (is_numeric($element)) {
603             return self::J_FLOAT;
604         }
605        
606         if ($element == 'true') {
607             return self::J_TRUE;
608         }
609        
610         if ($element == 'false') {
611             return self::J_FALSE;
612         }
613        
614         if ($element == 'null') {
615             return self::J_NULL;
616         }
617        
618         $last_stack = end($stack);
619         if ($element == ',' && $last_stack[0] == self::J_ARRAY_OPEN) {
620             return self::J_ARRAY_COMMA;
621         }
622        
623         if ($element == ',') {
624             return self::J_OBJ_COMMA;
625         }
626        
627         if ($element == ':') {
628             return self::J_COLON;
629         }
630        
631         if ($last == self::J_OBJ_OPEN || $last == self::J_OBJ_COMMA) {
632             return self::J_KEY;
633         }
634        
635         return self::J_STRING;
636     }
637    
638    
639     /**
640     * Created a unicode code point from a JS escaped unicode character
641     *
642     * @param array $match  A regex match containing the 4 digit code referenced by the key `1`
643     * @return string  The U+{digits} unicode code point
644     */
645     static private function makeUnicodeCodePoint($match)
646     {
647         return fUTF8::chr("U+" . $match[1]);
648     }
649    
650    
651     /**
652     * Sets the proper `Content-Type` header and outputs the value, encoded as JSON
653     *
654     * @param  mixed $value  The PHP value to output as JSON
655     * @return void
656     */
657     static public function output($value)
658     {
659         self::sendHeader();
660         echo self::encode($value);
661     }
662    
663    
664     /**
665     * Decodes a scalar value
666    
667     * @param  integer $type     The type of the element
668     * @param  string  $element  The element to be converted to a scalar
669     * @return mixed  The scalar value or the original string of the element
670     */
671     static private function scalarize($type, $element)
672     {
673         if ($type == self::J_INTEGER) {
674             $element = (integer) $element;
675         }
676         if ($type == self::J_FLOAT) {
677             $element = (float) $element;
678         }
679         if ($type == self::J_FALSE) {
680             $element = FALSE;
681         }
682         if ($type == self::J_TRUE) {
683             $element = TRUE;
684         }
685         if ($type == self::J_NULL) {
686             $element = NULL;
687         }
688         if ($type == self::J_STRING || $type == self::J_KEY) {
689             $element = substr($element, 1, -1);
690             $element = strtr($element, array_flip(self::$control_character_map));
691             $element = preg_replace_callback('#\\\\u([0-9a-fA-F]{4})#', array('self', 'makeUnicodeCodePoint'), $element);
692         }
693        
694         return $element;
695     }
696    
697    
698     /**
699     * Sets the proper `Content-Type` header for UTF-8 encoded JSON
700     *
701     * @return void
702     */
703     static public function sendHeader()
704     {
705         header('Content-Type: application/json; charset=utf-8');
706     }
707    
708    
709     /**
710     * Forces use as a static class
711     *
712     * @return fJSON
713     */
714     private function __construct() { }
715 }
716  
717  
718  
719 /**
720  * Copyright (c) 2008-2010 Will Bond <will@flourishlib.com>
721  *
722  * Permission is hereby granted, free of charge, to any person obtaining a copy
723  * of this software and associated documentation files (the "Software"), to deal
724  * in the Software without restriction, including without limitation the rights
725  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
726  * copies of the Software, and to permit persons to whom the Software is
727  * furnished to do so, subject to the following conditions:
728  *
729  * The above copyright notice and this permission notice shall be included in
730  * all copies or substantial portions of the Software.
731  *
732  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
733  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
734  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
735  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
736  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
737  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
738  * THE SOFTWARE.
739  */