Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
739 lines (642 sloc) 17 KB
<?php
/**
* Provides encoding and decoding for JSON
*
* This class is a compatibility class for the
* [http://php.net/json json extension] on servers with PHP 5.0 or 5.1, or
* servers with the json extension compiled out.
*
* This class will handle JSON values that are not contained in an array or
* object - such values are not valid according to the JSON spec, but the
* functionality is included for compatiblity with the json extension.
*
* @copyright Copyright (c) 2008-2010 Will Bond
* @author Will Bond [wb] <will@flourishlib.com>
* @license http://flourishlib.com/license
*
* @package Flourish
* @link http://flourishlib.com/fJSON
*
* @version 1.0.0b6
* @changes 1.0.0b6 Removed `e` flag from preg_replace() calls [wb, 2010-06-08]
* @changes 1.0.0b5 Added the ::output() method [wb, 2010-03-15]
* @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]
* @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]
* @changes 1.0.0b2 Changed @ error suppression operator to `error_reporting()` calls [wb, 2009-01-26]
* @changes 1.0.0b The initial implementation [wb, 2008-07-12]
*/
class fJSON
{
// The following constants allow for nice looking callbacks to static methods
const decode = 'fJSON::decode';
const encode = 'fJSON::encode';
const output = 'fJSON::output';
const sendHeader = 'fJSON::sendHeader';
/**
* An abstract representation of [
*
* @internal
*
* @var integer
*/
const J_ARRAY_OPEN = 0;
/**
* An abstract representation of , in a JSON array
*
* @internal
*
* @var integer
*/
const J_ARRAY_COMMA = 1;
/**
* An abstract representation of ]
*
* @internal
*
* @var integer
*/
const J_ARRAY_CLOSE = 2;
/**
* An abstract representation of {
*
* @internal
*
* @var integer
*/
const J_OBJ_OPEN = 3;
/**
* An abstract representation of a JSON object key
*
* @internal
*
* @var integer
*/
const J_KEY = 4;
/**
* An abstract representation of :
*
* @internal
*
* @var integer
*/
const J_COLON = 5;
/**
* An abstract representation of , in a JSON object
*
* @internal
*
* @var integer
*/
const J_OBJ_COMMA = 6;
/**
* An abstract representation of }
*
* @internal
*
* @var integer
*/
const J_OBJ_CLOSE = 7;
/**
* An abstract representation of an integer
*
* @internal
*
* @var integer
*/
const J_INTEGER = 8;
/**
* An abstract representation of a floating value
*
* @internal
*
* @var integer
*/
const J_FLOAT = 9;
/**
* An abstract representation of a boolean true
*
* @internal
*
* @var integer
*/
const J_TRUE = 10;
/**
* An abstract representation of a boolean false
*
* @internal
*
* @var integer
*/
const J_FALSE = 11;
/**
* An abstract representation of null
*
* @internal
*
* @var integer
*/
const J_NULL = 12;
/**
* An abstract representation of a string
*
* @internal
*
* @var integer
*/
const J_STRING = 13;
/**
* An array of special characters in JSON strings
*
* @var array
*/
static private $control_character_map = array(
'"' => '\"', '\\' => '\\\\', '/' => '\/', "\x8" => '\b',
"\xC" => '\f', "\n" => '\n', "\r" => '\r', "\t" => '\t'
);
/**
* An array of what values are allowed after other values
*
* @internal
*
* @var array
*/
static private $next_values = array(
self::J_ARRAY_OPEN => array(
self::J_ARRAY_OPEN => TRUE,
self::J_ARRAY_CLOSE => TRUE,
self::J_OBJ_OPEN => TRUE,
self::J_INTEGER => TRUE,
self::J_FLOAT => TRUE,
self::J_TRUE => TRUE,
self::J_FALSE => TRUE,
self::J_NULL => TRUE,
self::J_STRING => TRUE
),
self::J_ARRAY_COMMA => array(
self::J_ARRAY_OPEN => TRUE,
self::J_OBJ_OPEN => TRUE,
self::J_INTEGER => TRUE,
self::J_FLOAT => TRUE,
self::J_TRUE => TRUE,
self::J_FALSE => TRUE,
self::J_NULL => TRUE,
self::J_STRING => TRUE
),
self::J_ARRAY_CLOSE => array(
self::J_ARRAY_CLOSE => TRUE,
self::J_OBJ_CLOSE => TRUE,
self::J_ARRAY_COMMA => TRUE,
self::J_OBJ_COMMA => TRUE
),
self::J_OBJ_OPEN => array(
self::J_OBJ_CLOSE => TRUE,
self::J_KEY => TRUE
),
self::J_KEY => array(
self::J_COLON => TRUE
),
self::J_OBJ_COMMA => array(
self::J_KEY => TRUE
),
self::J_COLON => array(
self::J_ARRAY_OPEN => TRUE,
self::J_OBJ_OPEN => TRUE,
self::J_INTEGER => TRUE,
self::J_FLOAT => TRUE,
self::J_TRUE => TRUE,
self::J_FALSE => TRUE,
self::J_NULL => TRUE,
self::J_STRING => TRUE
),
self::J_OBJ_CLOSE => array(
self::J_ARRAY_CLOSE => TRUE,
self::J_OBJ_CLOSE => TRUE,
self::J_ARRAY_COMMA => TRUE,
self::J_OBJ_COMMA => TRUE
),
self::J_INTEGER => array(
self::J_ARRAY_CLOSE => TRUE,
self::J_OBJ_CLOSE => TRUE,
self::J_ARRAY_COMMA => TRUE,
self::J_OBJ_COMMA => TRUE
),
self::J_FLOAT => array(
self::J_ARRAY_CLOSE => TRUE,
self::J_OBJ_CLOSE => TRUE,
self::J_ARRAY_COMMA => TRUE,
self::J_OBJ_COMMA => TRUE
),
self::J_TRUE => array(
self::J_ARRAY_CLOSE => TRUE,
self::J_OBJ_CLOSE => TRUE,
self::J_ARRAY_COMMA => TRUE,
self::J_OBJ_COMMA => TRUE
),
self::J_FALSE => array(
self::J_ARRAY_CLOSE => TRUE,
self::J_OBJ_CLOSE => TRUE,
self::J_ARRAY_COMMA => TRUE,
self::J_OBJ_COMMA => TRUE
),
self::J_NULL => array(
self::J_ARRAY_CLOSE => TRUE,
self::J_OBJ_CLOSE => TRUE,
self::J_ARRAY_COMMA => TRUE,
self::J_OBJ_COMMA => TRUE
),
self::J_STRING => array(
self::J_ARRAY_CLOSE => TRUE,
self::J_OBJ_CLOSE => TRUE,
self::J_ARRAY_COMMA => TRUE,
self::J_OBJ_COMMA => TRUE
)
);
/**
* Decodes a JSON string into native PHP data types
*
* This function is very strict about the format of JSON. If the string is
* not a valid JSON string, `NULL` will be returned.
*
* @param string $json This should be the name of a related class
* @param boolean $assoc If this is TRUE, JSON objects will be represented as an assocative array instead of a `stdClass` object
* @return array|stdClass A PHP equivalent of the JSON string
*/
static public function decode($json, $assoc=FALSE)
{
if (!is_string($json) && !is_numeric($json)) {
return NULL;
}
$json = trim($json);
if ($json === '') {
return NULL;
}
// If the json is an array or object, we can rely on the php function
if (function_exists('json_decode') && ($json[0] == '[' || $json[0] == '{' || version_compare(PHP_VERSION, '5.2.9', '>='))) {
return json_decode($json, $assoc);
}
preg_match_all('~\[| # Array begin
\]| # Array end
{| # Object begin
}| # Object end
-?(?:0|[1-9]\d*) # Float
(?:\.\d*(?:[eE][+\-]?\d+)?|
(?:[eE][+\-]?\d+))|
-?(?:0|[1-9]\d*)| # Integer
true| # True
false| # False
null| # Null
,| # Member separator for arrays and objects
:| # Value separator for objects
"(?:(?:(?!\\\\u)[^\\\\"\n\b\f\r\t]+)| # String
\\\\\\\\|
\\\\/|
\\\\"|
\\\\b|
\\\\f|
\\\\n|
\\\\r|
\\\\t|
\\\\u[0-9a-fA-F]{4})*"|
\s+ # Whitespace
~x', $json, $matches);
$matched_length = 0;
$stack = array();
$last = NULL;
$last_key = NULL;
$output = NULL;
$container = NULL;
if (sizeof($matches) == 1 && strlen($matches[0][0]) == strlen($json)) {
$match = $matches[0][0];
$stack = array();
$type = self::getElementType($stack, self::J_ARRAY_OPEN, $match);
$element = self::scalarize($type, $match);
if ($match !== $element) {
return $element;
}
}
if ($json[0] != '[' && $json[0] != '{') {
return NULL;
}
foreach ($matches[0] as $match) {
if ($matched_length == 0) {
if ($match == '[') {
$output = array();
$last = self::J_ARRAY_OPEN;
} else {
$output = ($assoc) ? array() : new stdClass();
$last = self::J_OBJ_OPEN;
}
$stack[] = array($last, &$output);
$container =& $output;
$matched_length = 1;
continue;
}
$matched_length += strlen($match);
// Whitespace can be skipped over
if (ctype_space($match)) {
continue;
}
$type = self::getElementType($stack, $last, $match);
// An invalid sequence will cause parsing to stop
if (!isset(self::$next_values[$last][$type])) {
break;
}
// Decode the data values
$match = self::scalarize($type, $match);
// If the element is not a value, record some info and continue
if ($type == self::J_COLON ||
$type == self::J_OBJ_COMMA ||
$type == self::J_ARRAY_COMMA ||
$type == self::J_KEY) {
$last = $type;
if ($type == self::J_KEY) {
$last_key = $match;
}
continue;
}
// This flag is used to indicate if an array or object is being added and thus
// if the container reference needs to be changed to the current match
$ref_match = FALSE;
// Closing an object or array
if ($type == self::J_OBJ_CLOSE || $type == self::J_ARRAY_CLOSE) {
array_pop($stack);
if (sizeof($stack) == 0) {
break;
}
$new_container = end($stack);
$container =& $new_container[1];
$last = $type;
continue;
}
// Opening a new object or array requires some references to keep
// track of what the current container is
if ($type == self::J_OBJ_OPEN) {
$match = ($assoc) ? array() : new stdClass();
$ref_match = TRUE;
}
if ($type == self::J_ARRAY_OPEN) {
$match = array();
$ref_match = TRUE;
}
if ($ref_match) {
$stack[] = array($type, &$match);
$stack_end = end($stack);
}
// Here we assign the value. This code is kind of crazy because
// we have to keep track of the current container by references
// so we can traverse back down the stack as we move out of
// nested arrays and objects
if ($last == self::J_COLON && !$assoc) {
if ($last_key == '') {
$last_key = '_empty_';
}
if ($ref_match) {
$container->$last_key =& $stack_end[1];
$container =& $stack_end[1];
} else {
$container->$last_key = $match;
}
} elseif ($last == self::J_COLON) {
if ($ref_match) {
$container[$last_key] =& $stack_end[1];
$container =& $stack_end[1];
} else {
$container[$last_key] = $match;
}
} else {
if ($ref_match) {
$container[] =& $stack_end[1];
$container =& $stack_end[1];
} else {
$container[] = $match;
}
}
if ($last == self::J_COLON) {
$last_key = NULL;
}
$last = $type;
unset($match);
}
if ($matched_length != strlen($json) || sizeof($stack) > 0) {
return NULL;
}
return $output;
}
/**
* Encodes a PHP value into a JSON string
*
* @param mixed $value The PHP value to encode
* @return string The JSON string that is equivalent to the PHP value
*/
static public function encode($value)
{
if (is_resource($value)) {
return 'null';
}
if (function_exists('json_encode')) {
return json_encode($value);
}
if (is_int($value)) {
return (string) $value;
}
if (is_float($value)) {
return (string) $value;
}
if (is_bool($value)) {
return ($value) ? 'true' : 'false';
}
if (is_null($value)) {
return 'null';
}
if (is_string($value)) {
if (!preg_match('#^.*$#usD', $value)) {
return 'null';
}
$char_array = fUTF8::explode($value);
$output = '"';
foreach ($char_array as $char) {
if (isset(self::$control_character_map[$char])) {
$output .= self::$control_character_map[$char];
} elseif (strlen($char) < 2) {
$output .= $char;
} else {
$output .= '\u' . substr(strtolower(fUTF8::ord($char)), 2);
}
}
$output .= '"';
return $output;
}
// Detect if an array is associative, which would mean it needs to be encoded as an object
$is_assoc_array = FALSE;
if (is_array($value) && $value) {
$looking_for = 0;
foreach ($value as $key => $val) {
if (!is_numeric($key) || $key != $looking_for) {
$is_assoc_array = TRUE;
break;
}
$looking_for++;
}
}
if (is_object($value) || $is_assoc_array) {
$output = '{';
$members = array();
foreach ($value as $key => $val) {
$members[] = self::encode((string) $key) . ':' . self::encode($val);
}
$output .= join(',', $members);
$output .= '}';
return $output;
}
if (is_array($value)) {
$output = '[';
$members = array();
foreach ($value as $key => $val) {
$members[] = self::encode($val);
}
$output .= join(',', $members);
$output .= ']';
return $output;
}
}
/**
* Determines the type of a parser JSON element
*
* @param array &$stack The stack of arrays/objects being parsed
* @param integer $last The type of the last element parsed
* @param string $element The element being detected
* @return integer The element type
*/
static private function getElementType(&$stack, $last, $element)
{
if ($element == '[') {
return self::J_ARRAY_OPEN;
}
if ($element == ']') {
return self::J_ARRAY_CLOSE;
}
if ($element == '{') {
return self::J_OBJ_OPEN;
}
if ($element == '}') {
return self::J_OBJ_CLOSE;
}
if (ctype_digit($element)) {
return self::J_INTEGER;
}
if (is_numeric($element)) {
return self::J_FLOAT;
}
if ($element == 'true') {
return self::J_TRUE;
}
if ($element == 'false') {
return self::J_FALSE;
}
if ($element == 'null') {
return self::J_NULL;
}
$last_stack = end($stack);
if ($element == ',' && $last_stack[0] == self::J_ARRAY_OPEN) {
return self::J_ARRAY_COMMA;
}
if ($element == ',') {
return self::J_OBJ_COMMA;
}
if ($element == ':') {
return self::J_COLON;
}
if ($last == self::J_OBJ_OPEN || $last == self::J_OBJ_COMMA) {
return self::J_KEY;
}
return self::J_STRING;
}
/**
* Created a unicode code point from a JS escaped unicode character
*
* @param array $match A regex match containing the 4 digit code referenced by the key `1`
* @return string The U+{digits} unicode code point
*/
static private function makeUnicodeCodePoint($match)
{
return fUTF8::chr("U+" . $match[1]);
}
/**
* Sets the proper `Content-Type` header and outputs the value, encoded as JSON
*
* @param mixed $value The PHP value to output as JSON
* @return void
*/
static public function output($value)
{
self::sendHeader();
echo self::encode($value);
}
/**
* Decodes a scalar value
*
* @param integer $type The type of the element
* @param string $element The element to be converted to a scalar
* @return mixed The scalar value or the original string of the element
*/
static private function scalarize($type, $element)
{
if ($type == self::J_INTEGER) {
$element = (integer) $element;
}
if ($type == self::J_FLOAT) {
$element = (float) $element;
}
if ($type == self::J_FALSE) {
$element = FALSE;
}
if ($type == self::J_TRUE) {
$element = TRUE;
}
if ($type == self::J_NULL) {
$element = NULL;
}
if ($type == self::J_STRING || $type == self::J_KEY) {
$element = substr($element, 1, -1);
$element = strtr($element, array_flip(self::$control_character_map));
$element = preg_replace_callback('#\\\\u([0-9a-fA-F]{4})#', array('self', 'makeUnicodeCodePoint'), $element);
}
return $element;
}
/**
* Sets the proper `Content-Type` header for UTF-8 encoded JSON
*
* @return void
*/
static public function sendHeader()
{
header('Content-Type: application/json; charset=utf-8');
}
/**
* Forces use as a static class
*
* @return fJSON
*/
private function __construct() { }
}
/**
* Copyright (c) 2008-2010 Will Bond <will@flourishlib.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/